/* 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 #include #include #include #include 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; } 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; }