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

+94 -2
View File
@@ -26,10 +26,12 @@ typedef struct entry {
int64_t mtime;
} entry_t;
static int gb_to_bytes(int gb) {
/* 阶段3 顺手修:原返回 int,10GB(10*1024^3=10737418240)与 100GB 溢出 int 成 0/巨大值,
* 导致默认 10GB 上限实际"无上限"(evict 永不触发,缓存上限设置失效)——改 uint64_t 修真。 */
static uint64_t gb_to_bytes(int gb) {
if (gb < MIN_LIMIT_GB) gb = MIN_LIMIT_GB;
if (gb > MAX_LIMIT_GB) gb = MAX_LIMIT_GB;
return (int)((uint64_t)gb * 1024ULL * 1024ULL * 1024ULL);
return (uint64_t)gb * 1024ULL * 1024ULL * 1024ULL;
}
int ncm_cache_init(ncm_cache_t *c, const char *dir) {
@@ -140,6 +142,18 @@ const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w) {
return w ? w->tmp_path : "";
}
/* 半成品判定(阶段3,§5.1):.tmp(下载中)/ .part(中断残体)不属正式缓存条目,
* 不进 LRU 扫描/计数——否则正被下载播放的文件会被缓存上限误删(写者管理域)。 */
static int is_partial_name(const char *name)
{
if (!name) return 0;
size_t n = strlen(name);
const char *end = name + n;
if (n >= 4 && memcmp(end - 4, ".tmp", 4) == 0) return 1;
if (n >= 5 && memcmp(end - 5, ".part", 5) == 0) return 1;
return 0;
}
static int cmp_mtime_asc(const void *a, const void *b) {
const entry_t *x = (const entry_t *)a, *y = (const entry_t *)b;
if (x->mtime < y->mtime) return -1;
@@ -158,6 +172,7 @@ int ncm_cache_evict(const ncm_cache_t *c) {
struct dirent *e;
while ((e = readdir(d)) && n < MAX_ENTRIES) {
if (strncmp(e->d_name, "song_", 5) != 0) continue;
if (is_partial_name(e->d_name)) continue; /* 阶段3:.tmp/.part 归写者管理域,不淘汰 */
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name);
struct stat st;
@@ -184,6 +199,82 @@ int ncm_cache_evict(const ncm_cache_t *c) {
return NCM_OK;
}
/* ---- 阶段3:.part 断点续传(§5.2)---- */
/* .part 路径 = 正式文件路径 + ".part"(final+".part")。 */
static void part_path_of(const ncm_cache_t *c, const char *id, const char *ext,
char *part, size_t part_sz)
{
char final[NCM_CACHE_DIR_MAX + 64];
if (ncm_cache_path(c, id, ext, final, sizeof final) == 0) {
snprintf(part, part_sz, "%s.part", final);
} else {
part[0] = 0;
}
}
int ncm_cache_part_size(const ncm_cache_t *c, const char *id, const char *ext, uint64_t *size_out)
{
if (!c || !id || !size_out) return NCM_ERR_ARG;
char part[NCM_CACHE_DIR_MAX + 80];
part_path_of(c, id, ext, part, sizeof part);
if (!part[0]) return NCM_ERR_FULL;
struct stat st;
if (stat(part, &st) != 0 || !S_ISREG(st.st_mode)) return NCM_ERR_NOENT;
*size_out = (uint64_t)st.st_size;
return NCM_OK;
}
int ncm_cache_part_remove(const ncm_cache_t *c, const char *id, const char *ext)
{
if (!c || !id) return NCM_ERR_ARG;
char part[NCM_CACHE_DIR_MAX + 80];
part_path_of(c, id, ext, part, sizeof part);
if (!part[0]) return NCM_ERR_FULL;
unlink(part);
return NCM_OK;
}
int ncm_cache_open_resume(const ncm_cache_t *c, const char *id, const char *ext,
ncm_cache_writer_t **w_out)
{
if (!c || !id || !w_out) return NCM_ERR_ARG;
ncm_cache_writer_t *w = (ncm_cache_writer_t *)calloc(1, sizeof *w);
if (!w) return NCM_ERR_OOM;
w->fd = -1;
if (ncm_cache_path(c, id, ext, w->final_path, sizeof w->final_path) != 0) {
free(w); return NCM_ERR_FULL;
}
snprintf(w->tmp_path, sizeof w->tmp_path, "%s.part", w->final_path);
/* 追加打开已存在的 .part:保留已写字节,write 从尾部续(断点续传)。 */
w->fd = open(w->tmp_path, O_WRONLY | O_APPEND, 0644);
if (w->fd < 0) { free(w); return NCM_ERR_NOENT; }
struct stat st;
if (fstat(w->fd, &st) != 0 || st.st_size <= 0) {
close(w->fd); free(w); return NCM_ERR_NOENT;
}
w->nwritten = (uint64_t)st.st_size;
*w_out = w;
return NCM_OK;
}
/* 中断保留:音频路径专用(.tmp/.part → .part),供下次播放 Range 续传;
* nwritten==0 → 空残体无价值,直接 unlink。封面/测试仍用 ncm_cache_abort(=unlink)。 */
int ncm_cache_abort_part(ncm_cache_writer_t *w)
{
if (!w) return NCM_ERR_ARG;
if (w->fd >= 0) { close(w->fd); w->fd = -1; }
if (w->nwritten > 0) {
char part[NCM_CACHE_DIR_MAX + 80];
snprintf(part, sizeof part, "%s.part", w->final_path);
if (strcmp(w->tmp_path, part) != 0) rename(w->tmp_path, part); /* 已是 .part 则原位保留 */
} else {
unlink(w->tmp_path);
}
free(w);
return NCM_OK;
}
int ncm_cache_commit(ncm_cache_writer_t *w) {
if (!w) return NCM_ERR_ARG;
int rc = NCM_OK;
@@ -219,6 +310,7 @@ uint64_t ncm_cache_total_bytes(const ncm_cache_t *c) {
struct dirent *e;
while ((e = readdir(d))) {
if (strncmp(e->d_name, "song_", 5) != 0) continue;
if (is_partial_name(e->d_name)) continue; /* 阶段3:半成品不计入池占用 */
char p[NCM_CACHE_DIR_MAX + NAME_MAX + 8];
snprintf(p, sizeof p, "%s/%s", c->dir, e->d_name);
struct stat st;
+19 -2
View File
@@ -54,10 +54,27 @@ int ncm_cache_abort(ncm_cache_writer_t *w);
/* 写者 .tmp 路径(只读):边下边播解码侧用它开只读 fd 建 stream_src。 */
const char *ncm_cache_writer_tmp_path(const ncm_cache_writer_t *w);
/* 当前缓存总字节(扫描目录) */
/* ---- 阶段3:.part 断点续传 + 在播文件防淘汰(NCM_STREAM_PLAY §5) ----
* 命名约定:中断的 .tmp 保留为 final+".part"(残体),供下次播放「Range 续传」;
* 半成品 .tmp/.part 不参与 LRU 扫描/计数(evict/total_bytes 跳过),防正被下载播放的
* 文件被缓存上限挤掉。.tmp/.part 归写者自己的管理域。 */
/* 检测 .part 残体大小:存在且>0 → 写 *size_out 返回 NCM_OK;无 → NCM_ERR_NOENT。 */
int ncm_cache_part_size(const ncm_cache_t *c, const char *id, const char *ext, uint64_t *size_out);
/* 删除 .part 残体(续传目标已失效/服务器拒绝 Range 时放弃续传)。 */
int ncm_cache_part_remove(const ncm_cache_t *c, const char *id, const char *ext);
/* 续传打开:以「追加」打开已存在的 .part(final+".part",O_APPEND 保留已写字节,
* nwritten 置 part_size)。commit rename .part→final;abort(abort_part) 原地保留残体。
* part 不存在/空 → NCM_ERR_NOENT(调用方转整首重下)。 */
int ncm_cache_open_resume(const ncm_cache_t *c, const char *id, const char *ext,
ncm_cache_writer_t **w);
/* 中断保留:close fd 后 nwritten>0 → rename .tmp/.part → .part(跨 session 续传);
* nwritten==0 → unlink(空残体无价值)。音频路径用;封面/测试仍用 ncm_cache_abort。 */
int ncm_cache_abort_part(ncm_cache_writer_t *w);
/* 当前缓存总字节(扫描目录;跳过 .tmp/.part 半成品) */
uint64_t ncm_cache_total_bytes(const ncm_cache_t *c);
/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限)。 */
/* 立即执行 LRU 淘汰(超上限则删最旧,直到 <= 上限;跳过 .tmp/.part 半成品)。 */
int ncm_cache_evict(const ncm_cache_t *c);
/* 清空缓存池:删除 song_* 全部文件(mp3 与封面同池,含残留 .tmp)。 */
+12 -4
View File
@@ -253,6 +253,7 @@ typedef struct sink {
ncm_http_write_cb cb;
void *cb_ctx;
int overflow;
int *cb_status; /* 阶段3:parse_headers 后、首 body 前写入 resp->status(调用方续传分支用) */
} sink_t;
static void sink_write(sink_t *s, const unsigned char *data, size_t len) {
@@ -450,6 +451,7 @@ static int http_run(const ncm_http_req_t *req, sink_t *sink, ncm_http_resp_t *re
char *split = strstr(resp->headers, "\r\n\r\n");
if (split) {
parse_headers(resp);
if (sink->cb_status) *sink->cb_status = resp->status; /* 首 body 前预知状态(续传分支) */
body_st_init(&bst, resp);
/* split+4 即 body 起始;注意其与 resp->headers+consumed 是同一地址,
故先送 body 再在 consumed 处 NUL 终结头部,否则会覆盖 body 首字节。 */
@@ -512,17 +514,23 @@ int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz, ncm_ht
return rc;
}
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp) {
int ncm_http_download_status(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp, int *status_out) {
if (!req || !cb || !resp) return NCM_ERR_ARG;
if (status_out) *status_out = 0;
memset(resp, 0, sizeof *resp);
sink_t s;
memset(&s, 0, sizeof s);
s.cb = cb;
s.cb_ctx = ctx;
s.max_bytes = max_bytes;
int rc = http_run(req, &s, resp);
return rc;
s.cb_status = status_out;
return http_run(req, &s, resp);
}
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp) {
return ncm_http_download_status(req, cb, ctx, max_bytes, resp, NULL);
}
int ncm_http_parse_set_cookie(const char *hdrs, size_t len, char *out, size_t out_sz) {
+5
View File
@@ -64,6 +64,11 @@ int ncm_http_request(const ncm_http_req_t *req, char *buf, size_t buf_sz,
typedef int (*ncm_http_write_cb)(void *ctx, const unsigned char *data, size_t len);
int ncm_http_download(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp);
/* 流式下载(状态感知,阶段3 续传):与 ncm_http_download 相同,但在**首个 body 字节
* 投递前**把解析出的 resp->status 写入 *status_out(同线程、同步),调用方回调据此
* 预知 200/206/416 分支做写姿态切换。*status_out 可为 NULL。 */
int ncm_http_download_status(const ncm_http_req_t *req, ncm_http_write_cb cb, void *ctx,
size_t max_bytes, ncm_http_resp_t *resp, int *status_out);
/* 从响应头提取所有 Set-Cookie 的 name=value(去掉 Domain/Path/Expires 等属性),
* 以 "; " 连接写入 out(用于登录捕获 MUSIC_U 等)。成功返回 cookie 条数,失败 -1。 */