Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/player_flow_sim.c
T
MeowOS bdd9e898f4 feat(ncm §20): 音质体系全量——八档UI/VIP检测缓存/逐级降级下首恢复/fee门禁/默认极高HQ
- core: 9档枚举(顺序=降级链, DOLBY映射保留不进UI/链) + level_key(sky/sky_c51)
  + level_gate(CVIP/SVIP门槛) + song_url surround档附加immerseType=c51
  + song_url_auto全档表降级(out_level回填/响应level静默降档采信/try_fn可注入)
  + vip_info(weapi v2优先v1兜底) + parse_vip_info离线可测 + NCM_ERR_VIP(-15)
- app/logic/account.c(新): account.json会员态缓存(uid/nick/vip/双expire/checked_at)
  刷新=登录后/超1天/到期后各一次不轮询; account_vip本地到期降档视图
- player_manager: 用户档按会员钳顶后逐级降级(不硬拒,缓存键仍用户档),
  fee∈{1,4}且非会员→NCM_ERR_VIP零请求(缓存命中优先), eff_level回填
- UI: 设置页八档(规格+门槛+未解锁标注,允许选定), 播放页时间行中央
  生效档/已降级至xx小字, VIP专享文案; 默认档Hi-Res→极高HQ(存量设置保留)
- 验证(0真实请求): degrade_sim 90例矩阵全PASS; player_flow_sim P1-P8全PASS;
  test_ncm_core vip解析9例宿主+qemu全PASS; 窗口/分页/缓存/封面/audiod sim全PASS;
  mipsel -Wall -Wextra 0 warning; 6页离屏rc=0。未部署真机(留主会话)
2026-09-01 15:55:42 +08:00

396 lines
19 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 -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 落盘扩展名。
* §20:P7 覆盖 VIP 钳顶(非会员用户档 SURROUND → 请求档 EXHIGH,缓存键仍用户档 sky_c51;
* SVIP 直达)+ eff_level 回填;P8 覆盖 fee 门禁(fee=1 非会员 NCM_ERR_VIP 零请求 /
* 缓存命中绕过 / CVIP 放行 / fee=8 放行)。
*/
#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;
}
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, 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_* 分发层):
* 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/§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 */
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;
}