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
@@ -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;
|
||||
|
||||
@@ -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)。 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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。 */
|
||||
|
||||
Reference in new issue
Block a user