/* 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 清零。 * * 编译(宿主,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 -Wl,--gc-sections -o player_flow_sim /tmp/pf.o /tmp/pf_pm.o -lpthread -lm * 运行:./player_flow_sim (0 网络;PASS/FAIL 退出码) * §19:P6 覆盖缓存键=歌曲ID+档位 + flac type → .flac 落盘扩展名。 */ #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 ---- */ 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; return 0; } int appstate_song_fetch(int gidx, ncm_song_t *out) { return appstate_song_copy(gidx, out) == 0 ? NCM_OK : NCM_ERR_ARG; } /* ---- 缓存/网络 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 收到的扩展名 */ 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; } 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 : ""); *w = (ncm_cache_writer_t *)calloc(1, 64); return NCM_OK; } 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; return NCM_OK; } int ncm_cache_abort(ncm_cache_writer_t *w) { free(w); return NCM_OK; } 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; } int ncm_api_song_url_auto(const char *dev, const char *ck, const char *id, int level, ncm_play_url_t *out) { (void)dev; (void)ck; (void)id; (void)level; 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; 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) { (void)req; (void)maxb; g_downloads++; if (g_dl_delay_ms > 0) usleep((useconds_t)g_dl_delay_ms * 1000); unsigned char chunk[65536]; memset(chunk, 0x5A, sizeof chunk); for (int i = 0; i < 8; i++) /* 512KB 总分发(进度回调会走 8 次) */ if (cb(ctx, chunk, sizeof chunk) != 0) break; memset(resp, 0, sizeof *resp); resp->status = 200; return NCM_OK; } /* ---- 解码器 stub(§19:player_manager 改走 ncm_dec_* 分发层): * 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(ncm_decoder_t **out, const char *path, int *rate, int *ch, int *bits, int64_t *total_frames) { (void)path; *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:song_local_file 的缓存键用 ncm_api_level_str(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_str(int level) { switch (level) { 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; usleep(200 * 1000); player_manager_stop_thread(); printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n"); return g_fail; }