- 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。未部署真机(留主会话)
396 lines
19 KiB
C
396 lines
19 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 清零。
|
||
*
|
||
* 编译(宿主,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;
|
||
}
|