Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/player_flow_sim.c
T
MeowOS 6200a41b71 fix(ncm): 三连修 — 封面切歌不更新/缓冲无反馈失败无声/暂停切歌边界 (REPORT §18)
- cover worker 忙等死循环(成功后不清目标,每圈解码+换入+gen++,CPU 100%+每帧整盘重绘)
  → 成功清目标转怠速; 失败指数退避 0.5-4s 连 8 次放弃+WARN 日志; 去重加在拉目标;
  cover_get 持锁+cover_put 修绘制期 UAF; g_host_mock 防真网守卫
- 封面兜底污染修复: 不再把单曲 al_pic 写进 g_st.cover_url(否则 al_pic 缺失曲回落成
  上一首封面且撞去重 → 封面永停); plr_page_enter 强制重估自愈; pl_page_enter 回填歌单封面
- 缓冲 UI: BUFFER 态「缓冲中… N%」(g_play.dl_done/total, 下载回调累加); 播放错误中文
  错误条+snack(旧版 err 无 UI 消费者=无声死等); STOPPED 态播放键=重试; play/next 清 err
- 写失败 set_err 修连环跳歌(旧版静默 break 被外层当自然播完,流死时跳完歌单+狂发 song_url);
  still_current 护栏修下载途中切歌的状态/快照过期覆盖
- 验证(0 真实请求): cover_chain_sim 7 组+player_flow_sim P1-P5 全 PASS, 旧代码对照
  T2/P2/P4 必败坐实根因; audiod_chain_sim 14/14; pagination/window/core+qemu 回归 PASS;
  宿主+mipsel 自有代码 0 warning; 9 页离屏渲染 rc=0 (含新 mock 页 playerbuf/playererr)
- ncm md5 50d41bbf4a379e925699ba013b997ffd (未部署真机, 留主会话)
2026-09-01 05:29:09 +08:00

258 lines
12 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 -Ivendor/minimp3 -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 -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 退出码)
*/
#include "../app/ncm_app_internal.h"
#include "minimp3_ex.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 ---- */
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 请求放大检测) */
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; (void)id; (void)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.mp3", id);
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:g_eof 前恒有数据(P1 用 g_reads_left 控制播完点) ---- */
static int g_eof;
static int g_reads_left = -1; /* >=0: 剩余读次数后 EOF;-1: 直到 g_eof */
int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags)
{ (void)dec; (void)file_name; (void)flags; return 0; }
void mp3dec_ex_close(mp3dec_ex_t *dec) { (void)dec; }
int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position) { (void)dec; (void)position; return 0; }
size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples)
{
(void)dec;
if (g_eof) return 0;
if (g_reads_left == 0) return 0;
if (g_reads_left > 0) g_reads_left--;
memset(buf, 0, samples * sizeof(mp3d_sample_t));
return samples;
}
/* ---- 音频输出 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 short *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;
usleep(200 * 1000);
player_manager_stop_thread();
printf(g_fail ? "== PLAYER FLOW SIM: FAIL ==\n" : "== PLAYER FLOW SIM: ALL PASS ==\n");
return g_fail;
}