A. 返回首页各行 2~3s 空白:appstate_load_playlists 就地 memset(g_st.playlists[]) 与 home_src2 无锁逐行绑定竞态,刷新期间 playlist_count 保留旧值 → UI 读清零行。 照搬 s_build[] staging 范式:网络落 s_pls[] → 成功后锁内原子发布(拷回+更新count), 失败保留旧快照(断网离线浏览)。绝不就地清零 UI 可见数组。 B. 进第二个歌单显示上一歌单:playlist_open/refresh 同步路径不调 appstate_playlist_reset, pl_apply(1) 绑到上一歌单残留窗口(track_ids_count/songs)。 新增窗口归属(歌单 id 唯一标识, 对搜索歌单同样成立): worker 首屏发布后 appstate_playlist_window_set;refresh 目标≠归属(窗口is) → 同步 reset 再 pl_apply。 同歌单手动刷新(id 同)保留旧内容(stale-while-revalidate)。 宿主回归:playlist_race_sim(新 F 并发读无清零态/G 开A→开B 不串)、window/search/ cache_evict/player_flow/stream_sim、test_ncm_core 宿主+mipsel 全 PASS; build_ncm.sh -Wall -Wextra 0 warning;0 真实请求;未部署。
560 lines
30 KiB
C
560 lines
30 KiB
C
/* bench/player_flow_sim.c — 播放线程状态机离线回归(0 真实请求,REPORT §18)。
|
||
*
|
||
* 驱动真实 app/play/player_manager.c(外层 player_thread + run_song 全链路),
|
||
* 解码器/音频输出/网络/缓存全部本地 stub(可注入:写失败、url 失败、慢下载)。
|
||
*
|
||
* 用例:
|
||
* P1 自然播完 → 自动顺延下一首(曲终不停在原地,err=0);
|
||
* P2 音频写失败(流死/audiod 断链)→ **置错停播,cur_song 不动**——§18 前静默 break
|
||
* 被外层当「自然播完」连环跳歌+狂发 song_url(本用例在旧代码上 FAIL);
|
||
* P3 song_url 无音源 → STOPPED + NCM_ERR_NOTFOUND + errmsg(UI 有错误可显示);
|
||
* P4 下载途中切歌 → 旧曲下载完成后**不得**把状态改回 PLAYING/写过期快照
|
||
* (still_current 护栏;旧代码在切歌后 ~下载完成点 会把 BUFFER 覆盖成 PLAYING);
|
||
* P5 错误后 play(cur) 重试 → 状态重新走 BUFFER→PLAYING,err 清零。
|
||
* P12 切音质后重播同一曲(§25):同 idx 在播时再 play —— 修前被解码主循环忽略
|
||
* (旧曲继续出声、state 恒 BUFFER/「缓冲中」,收尾被外层当播完顺延下一首);
|
||
* 修后 seq 变化即重启换档,状态回 PLAYING、新档位下载、eff_level 回填。
|
||
*
|
||
* 编译(宿主,ncm_app 目录下):
|
||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||
* -Icore -Ivendor/jsmn -c bench/player_flow_sim.c -o /tmp/pf.o
|
||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||
* -Icore -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac -c app/play/player_manager.c -o /tmp/pf_pm.o
|
||
* gcc -O2 -ffunction-sections -DJSMN_HEADER -I. -I../sdk -I../meow_launcherd \
|
||
* -Icore -c app/play/stream_src.c -o /tmp/pf_ss.o
|
||
* gcc -O2 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o /tmp/pf_ss.o -lpthread -lm
|
||
* 运行:./player_flow_sim (0 网络;PASS/FAIL 退出码)
|
||
* §19:P6 覆盖缓存键=歌曲ID+档位 + flac type → .flac 落盘扩展名。
|
||
* §20:P7 覆盖 VIP 钳顶(非会员用户档 SURROUND → 请求档 EXHIGH,缓存键仍用户档 sky_c51;
|
||
* SVIP 直达)+ eff_level 回填;P8 覆盖 fee 门禁(fee=1 非会员 NCM_ERR_VIP 零请求 /
|
||
* 缓存命中绕过 / CVIP 放行 / fee=8 放行)。
|
||
* §21:边下边播——下载线程走 stub http_download(经真实 stream_src 预缓冲门槛),
|
||
* 解码器 stub 换 ncm_dec_open_src。
|
||
*/
|
||
#include "../app/ncm_app_internal.h"
|
||
#include "../app/play/decoder.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdarg.h>
|
||
#include <unistd.h>
|
||
|
||
ncm_app_state_t g_st;
|
||
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER, .state = NCM_PC_STOPPED, .cur_song = -1 };
|
||
volatile int g_app_quit = 0;
|
||
volatile int g_net_ready = 1;
|
||
int g_host_mock = 0;
|
||
|
||
/* ---- 公共 stub ---- */
|
||
size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
||
{
|
||
size_t n = 0;
|
||
if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; }
|
||
while (src[n]) n++;
|
||
return n;
|
||
}
|
||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; }
|
||
|
||
/* ---- appstate stub:窗内全命中,id=1000+idx;fee 可注入(§20 P8) ---- */
|
||
static int g_fee = 0;
|
||
int appstate_song_copy(int gidx, ncm_song_t *out)
|
||
{
|
||
if (gidx < 0 || gidx >= g_st.track_ids_count) return -1;
|
||
memset(out, 0, sizeof *out);
|
||
snprintf(out->id, sizeof out->id, "%d", 1000 + gidx);
|
||
snprintf(out->name, sizeof out->name, "Song%d", gidx);
|
||
out->duration_ms = 180000;
|
||
out->fee = g_fee;
|
||
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 文件,免下载);
|
||
* 否则走下载:song_url(g_url_rc 可注入失败) → http_download(g_dl_delay_ms 模拟慢) → commit。 */
|
||
static int g_cache_ok = 1;
|
||
static int g_url_rc = NCM_OK;
|
||
static int g_dl_delay_ms = 0;
|
||
static int g_downloads; /* http_download 调用数(song_url 请求放大检测) */
|
||
static const char *g_url_type = "mp3"; /* §19:song_url 注入的 type(mp3/flac) */
|
||
static char g_last_key[80]; /* ncm_cache_open 收到的键(档位键断言用) */
|
||
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; }
|
||
int account_level_cap(void)
|
||
{
|
||
switch (account_vip()) {
|
||
case NCM_VIP_SVIP: return NCM_LEVEL_SURROUND;
|
||
case NCM_VIP_CVIP: return NCM_LEVEL_HIRES;
|
||
default: return NCM_LEVEL_EXHIGH;
|
||
}
|
||
}
|
||
|
||
int ncm_cache_lookup(const ncm_cache_t *c, const char *id, const char *ext, char *out, size_t out_sz)
|
||
{
|
||
(void)c; (void)id; (void)ext;
|
||
if (!g_cache_ok) return NCM_ERR_NOENT;
|
||
snprintf(out, out_sz, "/tmp/pf_sim.mp3");
|
||
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 : "");
|
||
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; }
|
||
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)
|
||
{
|
||
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)
|
||
{
|
||
(void)dev; (void)ck; (void)id;
|
||
g_url_calls++;
|
||
g_url_req_level = level;
|
||
if (out_level) *out_level = -1;
|
||
if (g_url_rc != NCM_OK) return g_url_rc;
|
||
memset(out, 0, sizeof *out);
|
||
snprintf(out->url, sizeof out->url, "http://stub/%s.%s", id, g_url_type);
|
||
snprintf(out->type, sizeof out->type, "%s", g_url_type);
|
||
out->size = 4 * 1024 * 1024;
|
||
if (out_level) *out_level = level;
|
||
return NCM_OK;
|
||
}
|
||
|
||
/* §20.3(HEAD 的降级链工作)把 player_manager 切到 ncm_api_song_url_auto_av;本 sim 只测播放流
|
||
* 不测档位链语义,故转发到上方 auto 桩(desired→level),0 请求。与本次两处 UI 竞态无关。 */
|
||
int ncm_api_song_url_auto_av(const char *device_id, const char *cookie, const char *id,
|
||
int account_vip, int fee, int desired,
|
||
ncm_play_url_t *out, int *out_level)
|
||
{
|
||
(void)account_vip; (void)fee;
|
||
return ncm_api_song_url_auto(device_id, cookie, id, desired, out, out_level);
|
||
}
|
||
|
||
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);
|
||
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 控制播完点) ---- */
|
||
static int g_eof;
|
||
static int g_reads_left = -1; /* >=0: 剩余读次数后 EOF;-1: 直到 g_eof */
|
||
static int g_dec_open_bits = 16; /* 模拟流位宽(记录用) */
|
||
|
||
int ncm_dec_open_src(ncm_decoder_t **out, stream_src_t *src, int *rate, int *ch, int *bits,
|
||
int64_t *total_frames)
|
||
{
|
||
(void)src;
|
||
*out = (ncm_decoder_t *)calloc(1, 16);
|
||
if (rate) *rate = 44100;
|
||
if (ch) *ch = 2;
|
||
if (bits) *bits = g_dec_open_bits;
|
||
if (total_frames) *total_frames = 44100 * 180;
|
||
return *out ? 0 : -1;
|
||
}
|
||
int64_t ncm_dec_read(ncm_decoder_t *d, void *buf, int64_t cap_frames)
|
||
{
|
||
(void)d;
|
||
if (g_eof) return 0;
|
||
if (g_reads_left == 0) return 0;
|
||
if (g_reads_left > 0) g_reads_left--;
|
||
memset(buf, 0, (size_t)cap_frames * 2 * sizeof(int16_t));
|
||
return cap_frames;
|
||
}
|
||
int ncm_dec_seek_ms(ncm_decoder_t *d, int64_t ms, int64_t fb, int64_t dur)
|
||
{ (void)d; (void)ms; (void)fb; (void)dur; return 0; }
|
||
void ncm_dec_close(ncm_decoder_t *d) { free(d); }
|
||
const char *ncm_dec_format(const ncm_decoder_t *d) { (void)d; return "stub"; }
|
||
|
||
/* §19/§20:song_local_file 的缓存键用 ncm_api_level_key(g_level);ext 判定用 ncm_streq */
|
||
int ncm_streq(const char *a, const char *b)
|
||
{
|
||
if (!a || !b) return 0;
|
||
return strcmp(a, b) == 0;
|
||
}
|
||
const char *ncm_api_level_key(int level)
|
||
{
|
||
switch (level) {
|
||
case NCM_LEVEL_SURROUND: return "sky_c51";
|
||
case NCM_LEVEL_SKY: return "sky";
|
||
case NCM_LEVEL_JYMASTER: return "jymaster";
|
||
case NCM_LEVEL_JYEFFECT: return "jyeffect";
|
||
case NCM_LEVEL_HIRES: return "hires";
|
||
case NCM_LEVEL_LOSSLESS: return "lossless";
|
||
case NCM_LEVEL_EXHIGH: return "exhigh";
|
||
case NCM_LEVEL_HIGHER: return "higher";
|
||
default: return "standard";
|
||
}
|
||
}
|
||
|
||
/* ---- 音频输出 stub:g_write_fail 注入写失败 ---- */
|
||
static int g_write_fail;
|
||
static int g_writes;
|
||
static int g_opens; /* audio_out_open 次数(每首 run_song 开一次) */
|
||
|
||
int audio_out_open(int rate, int ch, int bits) { (void)rate; (void)ch; (void)bits; g_opens++; return 0; }
|
||
int audio_out_write(const void *s, int frames)
|
||
{ (void)s; g_writes++; return g_write_fail ? -1 : frames; }
|
||
int audio_out_pause(int paused) { (void)paused; return 0; }
|
||
void audio_out_close(void) { }
|
||
int audio_out_buffered_ms(void) { return 0; }
|
||
void audio_out_set_target(int ms) { (void)ms; }
|
||
|
||
/* ---- 歌词(未被调用的 worker 会被 gc-sections 丢弃;lyric_clear 由 worker 用,防御性提供) ---- */
|
||
void lyric_clear(void) { }
|
||
int lyric_parse(const char *a, const char *b) { (void)a; (void)b; return 0; }
|
||
|
||
static int g_fail;
|
||
#define CHECK(cond, fmt, ...) do { \
|
||
if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \
|
||
else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \
|
||
} while (0)
|
||
|
||
static int state(void) { return player_manager_state(); }
|
||
|
||
static int wait_state(int want, int ms)
|
||
{
|
||
for (int t = 0; t < ms; t += 10) { if (state() == want) return 1; usleep(10 * 1000); }
|
||
return 0;
|
||
}
|
||
|
||
static int wait_err(int want, int ms) /* 等错误码出现(初始态可能已是 STOPPED,等状态有竞态) */
|
||
{
|
||
for (int t = 0; t < ms; t += 10) { if (player_manager_error() == want) return 1; usleep(10 * 1000); }
|
||
return 0;
|
||
}
|
||
|
||
static int wait_downloads(int want, int ms)
|
||
{
|
||
for (int t = 0; t < ms; t += 10) { if (g_downloads >= want) return 1; usleep(10 * 1000); }
|
||
return 0;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
/* 假 mp3 文件(cache 命中路径 stat 用) */
|
||
FILE *f = fopen("/tmp/pf_sim.mp3", "wb");
|
||
if (!f) { printf("FAIL tmp file\n"); return 1; }
|
||
for (int i = 0; i < 4096; i++) fputc(i & 0xFF, f);
|
||
fclose(f);
|
||
|
||
memset(&g_st, 0, sizeof g_st);
|
||
g_st.track_ids_count = 3;
|
||
for (int i = 0; i < 3; i++) g_st.song_ids[i] = 1000 + i;
|
||
g_st.level = NCM_LEVEL_STANDARD;
|
||
g_st.cache_limit_gb = 10;
|
||
|
||
if (player_manager_start() != 0) { printf("FAIL start\n"); return 1; }
|
||
|
||
printf("== P1: natural finish -> auto-advance, err=0 ==\n");
|
||
g_cache_ok = 1; g_reads_left = -1; g_eof = 0; g_write_fail = 0;
|
||
player_manager_play(0);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 2000), "song0 playing (持续读流态可捕获)");
|
||
/* stub 解码/写瞬时完成,整列顺排在一次 poll 内跑完(cur 直接冲到 -1)——
|
||
* 改用 audio_out_open 计数判定:3 首全开过 = 自然顺延链走通。 */
|
||
g_reads_left = 1; /* song0 再读 1 次即 EOF → 自然播完 */
|
||
for (int t = 0; t < 4000 && state() != NCM_PC_STOPPED; t += 20) {
|
||
if (g_reads_left <= 0) g_reads_left = 1;
|
||
usleep(20 * 1000);
|
||
}
|
||
CHECK(g_opens == 3, "auto-advance chain: audio opens=%d (expect 3 首全开)", g_opens);
|
||
CHECK(state() == NCM_PC_STOPPED && player_manager_error() == 0,
|
||
"end of playlist -> STOPPED err=%d (expect 0)", player_manager_error());
|
||
|
||
printf("== P2: write fail (stream dead) -> STOPPED + err, cur unchanged (不连环跳歌) ==\n");
|
||
g_reads_left = -1; g_eof = 0; g_write_fail = 1; g_writes = 0;
|
||
player_manager_play(1);
|
||
CHECK(wait_err(NCM_ERR_IO, 2000), "err=NCM_ERR_IO after write fail (rc=%d)", player_manager_error());
|
||
CHECK(wait_state(NCM_PC_STOPPED, 1000), "stopped after write fail");
|
||
usleep(500 * 1000);
|
||
CHECK(player_manager_playing_song() == 1, "cur=%d (expect 1, 未跳歌)", player_manager_playing_song());
|
||
CHECK(state() == NCM_PC_STOPPED, "stays STOPPED (no cascade)");
|
||
CHECK(g_writes <= 2, "writes=%d (≤2: 第一次失败即停,不连环)", g_writes);
|
||
g_write_fail = 0;
|
||
|
||
printf("== P3: no stream url -> STOPPED + NOTFOUND + errmsg ==\n");
|
||
g_url_rc = NCM_ERR_NOTFOUND; g_cache_ok = 0;
|
||
player_manager_play(2);
|
||
CHECK(wait_err(NCM_ERR_NOTFOUND, 2000), "err=NOTFOUND on no-stream (rc=%d)", player_manager_error());
|
||
CHECK(wait_state(NCM_PC_STOPPED, 1000), "stopped on no-stream");
|
||
pthread_mutex_lock(&g_play.mu);
|
||
const char *em = g_play.errmsg;
|
||
int has_msg = em[0] != 0;
|
||
pthread_mutex_unlock(&g_play.mu);
|
||
CHECK(has_msg, "errmsg set (UI 可显示)");
|
||
g_url_rc = NCM_OK;
|
||
|
||
printf("== P4: switch during slow download -> no stale PLAYING overwrite ==\n");
|
||
g_cache_ok = 0; g_dl_delay_ms = 250; g_eof = 0; g_reads_left = -1;
|
||
player_manager_play(0);
|
||
CHECK(wait_downloads(1, 3000), "song0 download started");
|
||
player_manager_play(1); /* 旧曲下载途中切歌 */
|
||
/* 旧曲下载 +250ms 完成;新代码 still_current 护栏直接返回(状态保持 BUFFER),
|
||
* 旧代码会在此点把 state 覆盖成 PLAYING。+280ms 落在新曲下载窗口内(~+300 才开始)。 */
|
||
usleep(280 * 1000);
|
||
CHECK(state() == NCM_PC_BUFFER,
|
||
"after switch+during new download state=%d (expect BUFFER=%d; 旧代码被旧曲改成 PLAYING)",
|
||
state(), NCM_PC_BUFFER);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "new song eventually playing");
|
||
ncm_song_t snap;
|
||
CHECK(player_manager_current_song(&snap) == 0 && strcmp(snap.id, "1001") == 0,
|
||
"snapshot id=%s (expect 1001, 无过期快照)", snap.id);
|
||
g_eof = 1; /* 让新曲自然收尾 */
|
||
usleep(200 * 1000);
|
||
g_dl_delay_ms = 0;
|
||
|
||
printf("== P5: retry after error -> BUFFER->PLAYING, err cleared ==\n");
|
||
g_eof = 0; g_reads_left = -1; g_url_rc = NCM_ERR_NOTFOUND;
|
||
g_cache_ok = 0;
|
||
player_manager_play(2); /* 制造错误 */
|
||
CHECK(wait_err(NCM_ERR_NOTFOUND, 2000), "error state ready (rc=%d)", player_manager_error());
|
||
g_url_rc = NCM_OK; /* 网络恢复 */
|
||
player_manager_play(player_manager_playing_song()); /* 重试(UI 播放键路径) */
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "retry playing");
|
||
CHECK(player_manager_error() == 0, "err cleared on retry");
|
||
g_eof = 1;
|
||
|
||
printf("== P6: cache key = id+level, flac type -> .flac ext (§19) ==\n");
|
||
usleep(300 * 1000); /* 等 P5 曲收尾 */
|
||
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_dl_delay_ms = 0;
|
||
g_url_type = "flac";
|
||
player_manager_set_quality(NCM_LEVEL_LOSSLESS, 10); /* 换档 → g_level=lossless */
|
||
player_manager_play(0);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "flac download song playing");
|
||
CHECK(strcmp(g_last_key, "1000_lossless") == 0,
|
||
"cache key=%s (expect 1000_lossless = id+档位)", g_last_key);
|
||
CHECK(strcmp(g_last_ext, "flac") == 0,
|
||
"cache ext=%s (expect flac, type=flac → .flac 落盘)", g_last_ext);
|
||
g_url_type = "mp3";
|
||
player_manager_set_quality(NCM_LEVEL_STANDARD, 10); /* 还原,避免影响后续(无) */
|
||
g_eof = 1;
|
||
|
||
printf("== P7: VIP cap(§20 超权限档不硬拒,钳顶后降级;下首恢复用户档)==\n");
|
||
usleep(300 * 1000); /* 等 P6 曲收尾 */
|
||
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_dl_delay_ms = 0; g_fee = 0;
|
||
player_manager_set_quality(NCM_LEVEL_SURROUND, 10); /* 用户档=沉浸环绕声(SVIP 档) */
|
||
g_vip = NCM_VIP_NONE; g_url_calls = 0; g_url_req_level = -1;
|
||
player_manager_play(0);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "non-vip song playing (降级后照常播)");
|
||
CHECK(g_url_req_level == NCM_LEVEL_EXHIGH,
|
||
"req level=%d (expect EXHIGH=%d:非会员钳顶,不浪费高档请求)",
|
||
g_url_req_level, NCM_LEVEL_EXHIGH);
|
||
CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH,
|
||
"eff_level=%d (expect EXHIGH,UI 标「已降级至 极高 HQ」)", player_manager_eff_level());
|
||
CHECK(strcmp(g_last_key, "1000_sky_c51") == 0,
|
||
"cache key=%s (expect 1000_sky_c51 = id+用户档,降级产物归用户档键)", g_last_key);
|
||
/* SVIP:同一用户档不钳顶,直达 SURROUND。
|
||
* §21 时序:commit 移到 song0 曲尾 dispose(边下边播)——先 stop 阻断外层自动顺延
|
||
* 到 song1 的竞态,再等 commit 落地后才切缓存开关,否则 commit stub 会把
|
||
* g_cache_ok 重新置 1 / 自动顺延的 song1 会先发出请求 → 断言读数被污染。 */
|
||
g_eof = 1;
|
||
player_manager_stop();
|
||
{
|
||
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");
|
||
CHECK(g_url_req_level == NCM_LEVEL_SURROUND,
|
||
"req level=%d (expect SURROUND=%d:SVIP 不钳顶)", g_url_req_level, NCM_LEVEL_SURROUND);
|
||
CHECK(player_manager_eff_level() == NCM_LEVEL_SURROUND,
|
||
"eff_level=%d (expect SURROUND)", player_manager_eff_level());
|
||
g_eof = 1;
|
||
|
||
printf("== P8: fee 门禁(§20:fee∈{1,4} 且非会员 → NCM_ERR_VIP,不发 song_url)==\n");
|
||
usleep(300 * 1000);
|
||
g_eof = 0; g_reads_left = -1; g_cache_ok = 0; g_url_rc = NCM_OK;
|
||
g_fee = 1; g_vip = NCM_VIP_NONE; g_url_calls = 0;
|
||
player_manager_play(2);
|
||
CHECK(wait_err(NCM_ERR_VIP, 2000), "fee=1 + 非会员 -> err=NCM_ERR_VIP (rc=%d)",
|
||
player_manager_error());
|
||
CHECK(g_url_calls == 0, "song_url 零请求(省一次请求,防风控;calls=%d)", g_url_calls);
|
||
/* 已缓存的 VIP 曲:门禁不拦(缓存优先,NCM_CACHE_DESIGN §2.4) */
|
||
g_cache_ok = 1;
|
||
player_manager_play(2);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "cached VIP song plays (缓存命中绕过门禁)");
|
||
g_eof = 1; /* 先让它自然收尾(run_song 在跑时同 idx play 不重入) */
|
||
wait_state(NCM_PC_STOPPED, 3000);
|
||
/* CVIP:fee=1 放行,正常发请求 */
|
||
g_eof = 0; g_reads_left = -1;
|
||
g_cache_ok = 0; g_vip = NCM_VIP_CVIP; g_url_calls = 0;
|
||
player_manager_play(2);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "CVIP plays fee=1 song");
|
||
CHECK(g_url_calls == 1, "CVIP song_url 正常请求 (calls=%d)", g_url_calls);
|
||
g_eof = 1;
|
||
wait_state(NCM_PC_STOPPED, 3000);
|
||
/* fee=8(免费听低音质)非会员也放行 */
|
||
g_eof = 0; g_reads_left = -1;
|
||
g_cache_ok = 0; g_vip = NCM_VIP_NONE; g_fee = 8; g_url_calls = 0;
|
||
player_manager_play(2);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "fee=8 non-vip plays (免费听档放行)");
|
||
CHECK(g_url_calls == 1, "fee=8 song_url requested (calls=%d)", g_url_calls);
|
||
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;
|
||
|
||
printf("== P12: 切音质后重播同一曲(同 idx 在播时再 play)-> 换档重启,非卡「缓冲中」(§25)==\n");
|
||
usleep(300 * 1000);
|
||
g_eof = 0; g_reads_left = -1; g_cache_ok = 1; g_url_rc = NCM_OK; g_fee = 0;
|
||
g_part_size = 0; g_http_status = 200; g_416_first = 0; g_dl_delay_ms = 0; g_dl_fail = 0;
|
||
g_vip = NCM_VIP_NONE; g_writes = 0; g_downloads = 0; g_url_calls = 0; g_last_key[0] = 0;
|
||
/* 以 standard 档播放 song0(缓存命中,PLAYING) */
|
||
player_manager_set_quality(NCM_LEVEL_STANDARD, 10);
|
||
player_manager_play(0);
|
||
CHECK(wait_state(NCM_PC_PLAYING, 3000), "song0 playing at standard (cache hit)");
|
||
CHECK(g_writes > 0, "standard song actually emitting audio (writes=%d)", g_writes);
|
||
/* 切音质到 lossless,然后重播同一曲(同 idx=0 再次 play)。
|
||
* 新档位 = 新缓存条目(lookup 必 miss)→ 如实置 g_cache_ok=0 强制走下载(边下边播)。 */
|
||
player_manager_set_quality(NCM_LEVEL_LOSSLESS, 10);
|
||
g_cache_ok = 0;
|
||
player_manager_play(0);
|
||
/* §25 修前:同 idx 重播被解码主循环忽略(cur 不变、state 仍 BUFFER)→ 旧曲继续出声、
|
||
* 恒「缓冲中」,且旧曲收尾被外层当「自然播完」顺延下一首(新档位永不生效)。
|
||
* 修后:seq 变化即 break → 外层重跑 run_song(0) 换档下载 → 状态回到 PLAYING
|
||
* (UI 显示采样率/时间而非「缓冲中」)。 */
|
||
CHECK(wait_state(NCM_PC_PLAYING, 4000),
|
||
"re-tap same idx -> back to PLAYING (expect 换档开播,非卡 缓冲中)");
|
||
CHECK(g_downloads >= 1, "re-tap started new-quality download (downloads=%d)", g_downloads);
|
||
CHECK(strcmp(g_last_key, "1000_lossless") == 0,
|
||
"re-tap cache key=%s (expect 1000_lossless = 新档位)", g_last_key);
|
||
/* sim 账号非会员 → lossless 被 §20 钳顶到 EXHIGH;关键是 eff_level 已按新意图回填
|
||
* (UI 据此显示实际生效档,不再停 BUFFER/「缓冲中」)。 */
|
||
CHECK(player_manager_eff_level() == NCM_LEVEL_EXHIGH,
|
||
"re-tap eff_level=%d (expect EXHIGH=非会员钳顶的新档意图)", player_manager_eff_level());
|
||
g_eof = 1;
|
||
wait_state(NCM_PC_STOPPED, 3000);
|
||
|
||
usleep(200 * 1000);
|
||
player_manager_stop_thread();
|
||
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
|
||
return g_fail;
|
||
}
|