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
@@ -158,9 +158,15 @@ void player_manager_stop(void)
|
||||
|
||||
/* ---- 每首歌的播放体(在外层循环内调用)---- */
|
||||
|
||||
/* 边下边播每首曲的源对象(§21):读端 stream_src + 下载路径写端/线程。
|
||||
/* 边下边播每首曲的源对象(§21/阶段3):读端 stream_src + 下载路径写端/线程。
|
||||
* run_song 栈上分配,下载线程 join 后才返回,生命周期安全。 */
|
||||
#define PREBUFFER_BYTES (128 * 1024) /* 预缓冲门槛 = minimp3 MINIMP3_IO_SIZE(F3 实测强制) */
|
||||
#define PREBUFFER_BYTES (128 * 1024) /* 预缓冲门槛 = minimp3 MINIMP3_IO_SIZE(F3 实测强制) */
|
||||
#define STREAM_RETRY_MAX 1 /* 阶段3(§21.8/§4.1):首开声前失败重试上限(1 次,防风控) */
|
||||
#define STREAM_RETRY_BACKOFF_MS 800 /* 重试退避:避免连环请求轰炸(真实 NCM 节制,§7 风控) */
|
||||
|
||||
/* song_download_attempt 返回哨兵(与 NCM_ERR_* 不冲突) */
|
||||
#define DL_RETRYABLE (-1001) /* 预缓冲前网络失败:可整首重试 */
|
||||
#define DL_ABORTED (-1002) /* 用户切歌/退出:静默放弃,不重试 */
|
||||
|
||||
typedef struct {
|
||||
stream_src_t *src; /* 读端(下载路径 written 由下载线程推进;缓存命中=静态源) */
|
||||
@@ -171,14 +177,37 @@ typedef struct {
|
||||
char url[1024];/* 下载线程用(song_local_file 栈上 u.url 生命周期不够,拷贝) */
|
||||
size_t maxb; /* 下载限幅 */
|
||||
int64_t total; /* 标称大小(song_url size;0=未知;mp3 seek 分母) */
|
||||
/* 阶段3 .part 续传(§5.2) */
|
||||
uint64_t resume_base; /* >0=断点续传起点(.part 大小);Http 带 Range: bytes=<n>- */
|
||||
int first_body; /* 首个 body 回调未处理(按响应状态切换写姿态) */
|
||||
int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */
|
||||
int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */
|
||||
uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */
|
||||
} song_src_t;
|
||||
|
||||
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
|
||||
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。 */
|
||||
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。
|
||||
* 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态——
|
||||
* 206(服务器支持 Range):正常追加到 .part 尾;
|
||||
* 200(服务器忽略 Range):body 头 resume_base 字节与 .part 前缀重复 → skip 丢弃;
|
||||
* 416/其它:放弃续传(need_fresh=1,abort),交调用方删残体整首重下。 */
|
||||
static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
|
||||
{
|
||||
song_src_t *ss = (song_src_t *)ctx;
|
||||
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
|
||||
if (ss->first_body) {
|
||||
ss->first_body = 0;
|
||||
int st = ss->st_ptr ? *ss->st_ptr : 0;
|
||||
if (ss->resume_base > 0 && st != 206 && st != 200) {
|
||||
ss->need_fresh = 1; /* 416/其它:放弃续传 */
|
||||
return 1; /* abort http(error body 不入盘) */
|
||||
}
|
||||
if (ss->resume_base > 0 && st == 200) ss->skip = ss->resume_base; /* 全量头段与 .part 重复 */
|
||||
}
|
||||
if (ss->skip) { /* 丢弃重复前缀字节 */
|
||||
if (ss->skip >= len) { ss->skip -= len; return 0; }
|
||||
data += (size_t)ss->skip; len -= (size_t)ss->skip; ss->skip = 0;
|
||||
}
|
||||
if (ncm_cache_write(ss->w, data, len) != NCM_OK) return 1;
|
||||
stream_src_note_written(ss->src, (uint64_t)len);
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
@@ -194,14 +223,35 @@ static void *dl_thread(void *arg)
|
||||
memset(&req, 0, sizeof req);
|
||||
req.url = ss->url;
|
||||
req.method = "GET";
|
||||
req.extra_hdrs = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||
/* 续传:Range 头(§5.2)。URL 为本次新拉签名(不复用旧 URL,风控不增请求)。
|
||||
* UA 现状保留;Connection: close 由 http 层补。 */
|
||||
char hdrs[1200];
|
||||
if (ss->resume_base > 0)
|
||||
snprintf(hdrs, sizeof hdrs,
|
||||
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\n"
|
||||
"Range: bytes=%llu-", (unsigned long long)ss->resume_base);
|
||||
else
|
||||
snprintf(hdrs, sizeof hdrs,
|
||||
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
|
||||
req.extra_hdrs = hdrs;
|
||||
req.verify_ca = NCM_HTTP_VERIFY_DEFAULT;
|
||||
req.timeout_ms = 15000; /* 绝对段(连接/握手/头)上限;body 空闲超时 15s(P2) */
|
||||
|
||||
/* 状态感知下载:首 body 前写入 *status(续传分支)。同线程,st_ptr 无竞态。 */
|
||||
int status = 0;
|
||||
ss->st_ptr = &status;
|
||||
ss->first_body = 1;
|
||||
ncm_http_resp_t resp;
|
||||
int rc = ncm_http_download(&req, stream_dl_cb, ss, ss->maxb, &resp);
|
||||
if (rc == NCM_OK && resp.status != 200) rc = NCM_ERR_HTTP;
|
||||
int rc = ncm_http_download_status(&req, stream_dl_cb, ss, ss->maxb, &resp, &status);
|
||||
ss->st_ptr = NULL;
|
||||
if (rc == NCM_OK) {
|
||||
/* 放行 200 与(续传时的)206;其余按 HTTP 错。续传遇 200=服务器忽略 Range(skip 已丢头段)。 */
|
||||
int ok_st = (ss->resume_base > 0) ? (resp.status == 206 || resp.status == 200)
|
||||
: (resp.status == 200);
|
||||
if (!ok_st) rc = NCM_ERR_HTTP;
|
||||
}
|
||||
ss->dl_rc = rc;
|
||||
/* 结束通知:成功 final=全长;失败/中止 final=截断点+failed(读端播到截断点自然结束,F5) */
|
||||
stream_src_finish(ss->src, rc == NCM_OK ? 0 : rc);
|
||||
@@ -209,7 +259,7 @@ static void *dl_thread(void *arg)
|
||||
}
|
||||
|
||||
/* 收尾(播放线程,解码循环退出后调用):中止在途下载并 join → commit(rename 转正) 或
|
||||
* abort(unlink .tmp)。阶段 2 语义:中断即丢弃;.part 续传/LRU 防淘汰留阶段 3。 */
|
||||
* abort(阶段3:中断保留 .part 供下次续传;拒绝续传的 416 删残体)。 */
|
||||
static void song_src_dispose(song_src_t *ss)
|
||||
{
|
||||
if (ss->dl_thr_on) {
|
||||
@@ -222,8 +272,12 @@ static void song_src_dispose(song_src_t *ss)
|
||||
/* 下载完整:commit 转正进 LRU 池(rename 不影响读端已开 fd,此处已近曲终) */
|
||||
if (ncm_cache_commit(ss->w) == 0)
|
||||
ncm_cache_evict(&g_st.ctx.cache); /* 写后检查:新落盘可能推过上限 */
|
||||
} else {
|
||||
} else if (ss->need_fresh) {
|
||||
/* 服务器拒绝续传(416/其它):残体不可再续,删(下首整首重下) */
|
||||
ncm_cache_abort(ss->w);
|
||||
} else {
|
||||
/* 中断(切歌/断流):保留 .part 残体,供下次播放 Range 续传(§5.2) */
|
||||
ncm_cache_abort_part(ss->w);
|
||||
}
|
||||
ss->w = NULL;
|
||||
}
|
||||
@@ -248,6 +302,104 @@ static int still_current(int idx)
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* 一次「取 URL → 开写端(命中 .part 则续传) → 起下载线程 → 等预缓冲 ≥128KB」尝试(阶段3)。
|
||||
* 返回:NCM_OK=src 就绪(下载在后台继续);DL_RETRYABLE=预缓冲前网络失败(调用方整首重试);
|
||||
* DL_ABORTED=切歌/退出(静默放弃,不重试);其它负数=硬错误(不重试)。
|
||||
* *real_err 回填预缓冲失败的真实错误码。失败时 ss->w/src/dl_thr 已清理干净。 */
|
||||
static int song_download_attempt(const ncm_song_t *s, int idx, const char *key, int want,
|
||||
song_src_t *ss, char *path, size_t psz, int64_t *bytes,
|
||||
int *real_err)
|
||||
{
|
||||
const char *id = s->id;
|
||||
ss->resume_base = 0; ss->skip = 0; ss->need_fresh = 0; ss->first_body = 1; /* 每次尝试干净 */
|
||||
/* 播放地址:用户档已在调用方按会员钳顶,此处逐档降级,接受 mp3 与 flac */
|
||||
ncm_play_url_t u;
|
||||
int eff = -1;
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, want, &u, &eff);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
|
||||
const char *ext = "mp3";
|
||||
if (ncm_streq(u.type, "flac")) ext = "flac";
|
||||
else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac";
|
||||
|
||||
/* 写前检查:池大小超限先按 LRU 淘汰(半成品 .tmp/.part 已被 evict 跳过,无在途误删风险) */
|
||||
ncm_cache_evict(&g_st.ctx.cache);
|
||||
|
||||
/* 断点续传起点(§5.2):.part 残体存在且本次 URL 的 size > 残体大小 → 续传:
|
||||
* 追加打开 .part(O_APPEND,保留已写字节),written 从 part_size 起算(读端秒开播)。 */
|
||||
uint64_t part_size = 0;
|
||||
int have_part = (ncm_cache_part_size(&g_st.ctx.cache, key, ext, &part_size) == NCM_OK) &&
|
||||
part_size > 0;
|
||||
int resuming = 0;
|
||||
if (have_part && (long)part_size < u.size) {
|
||||
rc = ncm_cache_open_resume(&g_st.ctx.cache, key, ext, &ss->w);
|
||||
if (rc == NCM_OK) { ss->resume_base = part_size; resuming = 1; }
|
||||
else have_part = 0; /* 残体打开失败:转整首 */
|
||||
}
|
||||
if (!resuming) {
|
||||
if (have_part) ncm_cache_part_remove(&g_st.ctx.cache, key, ext); /* 残体过期(size 不符):删 */
|
||||
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w);
|
||||
if (rc != NCM_OK) return rc;
|
||||
}
|
||||
if (stream_src_open_stream_at(&ss->src, ncm_cache_writer_tmp_path(ss->w),
|
||||
resuming ? part_size : 0) != 0) {
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
return NCM_ERR_IO;
|
||||
}
|
||||
ncm_strlcpy(path, ncm_cache_writer_tmp_path(ss->w), psz); /* 日志用 */
|
||||
ss->total = (u.size > 0) ? u.size : 0;
|
||||
/* 上限:Hi-Res flac 单曲可达数百 MB,flac 上限 1GB;mp3 维持 64MB。
|
||||
* size 可信时按 size 限幅(防超额),未知按格式默认上限(续传 body ≤ size 亦安全)。 */
|
||||
size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20);
|
||||
ss->maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap;
|
||||
ncm_strlcpy(ss->url, u.url, sizeof ss->url);
|
||||
if (pthread_create(&ss->dl_thr, NULL, dl_thread, ss) != 0) {
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return NCM_ERR_IO;
|
||||
}
|
||||
ss->dl_thr_on = 1;
|
||||
|
||||
/* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。
|
||||
* still_current 护栏覆盖本阻塞点:等待期间切歌/停止 → 保留 .part 静默放弃。 */
|
||||
for (;;) {
|
||||
int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100);
|
||||
if (wr != 0) break;
|
||||
if (g_app_quit || !still_current(idx)) {
|
||||
stream_src_abort(ss->src);
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
ncm_cache_abort_part(ss->w); ss->w = NULL; /* 切歌/退出:保留残体供下次续传 */
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return DL_ABORTED; /* 调用方 still_current 复核后静默返回 */
|
||||
}
|
||||
}
|
||||
if (stream_src_aborted(ss->src)) {
|
||||
int frc = stream_src_failed(ss->src);
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
ncm_cache_abort_part(ss->w); ss->w = NULL; /* 中止:保留残体 */
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return frc ? frc : NCM_ERR_NET;
|
||||
}
|
||||
if (stream_src_failed(ss->src) && stream_src_written(ss->src) < PREBUFFER_BYTES) {
|
||||
int frc = stream_src_failed(ss->src);
|
||||
if (real_err) *real_err = frc;
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
/* 预缓冲前网络失败:删残体(不保留 .part),交调用方整首重试(含续传失败重下)。
|
||||
* 已下过门槛的断流照常放行——读端播到截断点自然结束(F5)。 */
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return DL_RETRYABLE;
|
||||
}
|
||||
/* final < 门槛(小文件/截断):照样放行,open_cb 读到的就是全部 */
|
||||
*bytes = ss->total > 0 ? ss->total : (int64_t)stream_src_written(ss->src);
|
||||
return NCM_OK;
|
||||
}
|
||||
|
||||
/* 得到本地音频源(§21 边下边播)。命中缓存 → 静态 stream_src(final=size,read 不阻塞,
|
||||
* 顺带修 P1:不再 mmap 整文件);否则 URL 降级 → 起下载线程写 .tmp → 预缓冲 ≥128KB 即返回
|
||||
* (下载在后台继续,解码经 stream_src read_cb 跟播)。
|
||||
@@ -302,75 +454,28 @@ static int song_local_file(const ncm_song_t *s, int idx, song_src_t *ss,
|
||||
if ((s->fee == 1 || s->fee == 4) && account_vip() < NCM_VIP_CVIP)
|
||||
return NCM_ERR_VIP;
|
||||
|
||||
/* 播放地址:用户档先按会员权限钳顶,再逐档降级(§20 全档表),接受 mp3 与 flac */
|
||||
/* 播放下载(阶段3 重试策略 §4.1,风控 §7:真实 NCM 请求节制):用户档先按会员权限
|
||||
* 钳顶,再逐档降级(§20)。预缓冲前网络失败重试 STREAM_RETRY_MAX 次(退避
|
||||
* STREAM_RETRY_BACKOFF_MS 防连环轰炸),重试每次重新 song_url(新签名 URL,eapi 便宜);
|
||||
* 硬错误/切歌/退出不重试。返回值:NCM_OK=src 就绪;DL_ABORTED=切歌/退出;其它负=错误。 */
|
||||
int want = g_level;
|
||||
int vip_cap = account_level_cap();
|
||||
if (want > vip_cap) want = vip_cap;
|
||||
ncm_play_url_t u;
|
||||
int eff = -1;
|
||||
int rc = ncm_api_song_url_auto(g_st.ctx.device_id, g_st.ctx.cookie, id, want, &u, &eff);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (!u.url[0]) return NCM_ERR_NOTFOUND;
|
||||
pthread_mutex_lock(&g_play.mu);
|
||||
g_play.eff_level = (eff >= 0) ? eff : want;
|
||||
g_play.dl_total = (u.size > 0) ? u.size : 0; /* 缓冲进度% 分母(0=未知只转菊花) */
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
/* 落盘扩展名:type 权威;type 缺失时看 url 扩展名,默认 mp3 */
|
||||
const char *ext = "mp3";
|
||||
if (ncm_streq(u.type, "flac")) ext = "flac";
|
||||
else if (!u.type[0] && strstr(u.url, ".flac")) ext = "flac";
|
||||
|
||||
/* 写前检查:池大小超限先按 LRU 淘汰(此线程尚未开 temp,无在途文件被误删风险) */
|
||||
ncm_cache_evict(&g_st.ctx.cache);
|
||||
|
||||
/* 边下边播(§21):下载线程写 .tmp(write(2) 直写),解码线程 pread 同一文件 */
|
||||
rc = ncm_cache_open(&g_st.ctx.cache, key, ext, &ss->w);
|
||||
if (rc != NCM_OK) return rc;
|
||||
if (stream_src_open_stream(&ss->src, ncm_cache_writer_tmp_path(ss->w)) != 0) {
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
return NCM_ERR_IO;
|
||||
}
|
||||
ncm_strlcpy(path, ncm_cache_writer_tmp_path(ss->w), psz); /* 日志用 */
|
||||
ss->total = (u.size > 0) ? u.size : 0;
|
||||
/* 上限:Hi-Res flac 单曲可达数百 MB(192k/24bit 5min ≈345MB),flac 上限 1GB;mp3 维持 64MB。
|
||||
* size 可信时按 size 限幅(防超额),未知按格式默认上限。 */
|
||||
size_t cap = (ext[0] == 'f') ? (1024u << 20) : (64u << 20);
|
||||
ss->maxb = (u.size > 0 && (uint64_t)u.size < cap) ? (size_t)u.size : cap;
|
||||
ncm_strlcpy(ss->url, u.url, sizeof ss->url);
|
||||
if (pthread_create(&ss->dl_thr, NULL, dl_thread, ss) != 0) {
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return NCM_ERR_IO;
|
||||
}
|
||||
ss->dl_thr_on = 1;
|
||||
|
||||
/* 预缓冲:≥128KB(minimp3 强制门槛,F3)才放行 open_cb;final/failed 提前出。
|
||||
* still_current 护栏覆盖本阻塞点:等待期间切歌/停止 → 中止下载静默放弃。 */
|
||||
for (;;) {
|
||||
int wr = stream_src_wait(ss->src, PREBUFFER_BYTES, 100);
|
||||
if (wr != 0) break;
|
||||
if (g_app_quit || !still_current(idx)) {
|
||||
stream_src_abort(ss->src);
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return NCM_ERR_IO; /* 调用方 still_current 复核后静默返回 */
|
||||
int last_rc = NCM_ERR_NET;
|
||||
for (int attempt = 0; attempt <= STREAM_RETRY_MAX; attempt++) {
|
||||
if (attempt > 0) {
|
||||
meow_log(MEOW_LOG_WARN, "play: download retry %d/%d id=%s backoff=%dms",
|
||||
attempt, STREAM_RETRY_MAX, id, STREAM_RETRY_BACKOFF_MS);
|
||||
usleep(STREAM_RETRY_BACKOFF_MS * 1000); /* 退避:不轰炸 */
|
||||
if (g_app_quit || !still_current(idx)) return NCM_ERR_IO;
|
||||
}
|
||||
int real_err = NCM_ERR_NET;
|
||||
int rc = song_download_attempt(s, idx, key, want, ss, path, psz, bytes, &real_err);
|
||||
if (rc == NCM_OK) return NCM_OK;
|
||||
if (rc == DL_RETRYABLE) { last_rc = real_err; continue; } /* 重试整首 */
|
||||
return rc; /* DL_ABORTED / 硬错误:不重试 */
|
||||
}
|
||||
if (stream_src_aborted(ss->src) ||
|
||||
(stream_src_failed(ss->src) && stream_src_written(ss->src) < PREBUFFER_BYTES)) {
|
||||
/* 中止,或首开声前失败(written < 预缓冲门槛):abort .tmp
|
||||
*(阶段 2 不保留不重试;重试策略留阶段 3)。已下过门槛的断流照常放行——
|
||||
* 读端播到截断点自然结束(F5)。 */
|
||||
int frc = stream_src_failed(ss->src);
|
||||
pthread_join(ss->dl_thr, NULL); ss->dl_thr_on = 0;
|
||||
ncm_cache_abort(ss->w); ss->w = NULL;
|
||||
stream_src_close(ss->src); ss->src = NULL;
|
||||
return frc ? frc : NCM_ERR_NET;
|
||||
}
|
||||
/* final < 门槛(小文件/截断):照样放行,open_cb 读到的就是全部 */
|
||||
*bytes = ss->total > 0 ? ss->total : (int64_t)stream_src_written(ss->src);
|
||||
return NCM_OK;
|
||||
return last_rc; /* 重试耗尽:最后一次网络错误 */
|
||||
}
|
||||
|
||||
static void set_err(int err, const char *msg)
|
||||
|
||||
Reference in new issue
Block a user