Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/player_flow_sim.c
T

414 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 -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; }
/* ---- 缓存/网络 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 注入的会员态 */
/* §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 要真实打开) */
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 : "");
FILE *f = fopen(g_tmp_path, "wb"); /* 建空 .tmp:stream_src_open_stream 需存在 */
if (f) fclose(f);
*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; }
static int g_commits; /* §21:commit 移到曲尾 dispose(边下边播),P7 等收尾用 */
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) { 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, 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;
}
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_* 分发层;§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_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);
usleep(200 * 1000);
player_manager_stop_thread();
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
return g_fail;
}