Files
MeowOS-For-R3P2/patch_v2/ncm_app/app/logic/appstate.c
T
MeowOS e0136213fe ncm A2: 音频输出迁移 meow_audiod(SINK_AUDIOD 默认后端)— 修无声根因
- audio_out.c: 新增 SINK_AUDIOD 默认(SDK meow_audio_* 连 audiod);NCM_AUDIO_SINK=file/null/alsa
  与 MEOW_AUDIO_DIRECT=1 保留调试后端;ALSA 保底修两大坑(hw_params 全放行初始化 + mask 清零
  再单点)——旧 memset0 interval=「只能取0」→ HW_PARAMS 恒 EINVAL 即播放无声根因
- 新增 audio_out_pause:PAUSED 沿向 audiod 发 pause(停时钟不丢流);resume 流死自动重开
- player_manager: 注册 meow_audio_on_preempt → g_play 命令通道暂停;播放事件全量 meow_log
  (open 参数/grant_*/write 错误节流/切歌/暂停/seek/done)
- appstate/app: /tmp/ncm_autoplay 真机联调 hook(仅播已缓存曲目,绝不发 song_url)
- bench/audiod_chain_sim.c: 真实 audiod_host 假 socket 全链离线回归 14 项全 PASS
- 验证: host 0 warning / WAV 回归 / qemu core PASS / mipsel 0 warning;真机 OPEN→grant 已通,
  持续写帧被 M2.90 设备 PCM 播放内核硬挂死阻断(REPORT §16.3,4/4 复现,留主会话)
- ncm md5 db5b78ad9e8f40e6306484efe2a08eb3(已热更设备)
2026-09-01 01:17:21 +08:00

295 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.
/* logic/appstate.c — 应用全局状态:ctx/登录/歌单/歌曲/设置。仅依赖核心库 + SDK meow_config_*。 */
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data"
int appstate_init(void)
{
const char *dd = getenv("MEOW_DATA_DIR");
if (!dd || !dd[0]) dd = NCM_DEFAULT_DATA;
strncpy(g_st.data_dir, dd, sizeof g_st.data_dir - 1);
g_st.data_dir[sizeof g_st.data_dir - 1] = 0;
ncm_ctx_init(&g_st.ctx, g_st.data_dir); /* device_id + cookie 载入 + cache */
if (g_st.ctx.cache.dir[0])
strncpy(g_st.cache_dir, g_st.ctx.cache.dir, sizeof g_st.cache_dir - 1);
else
ncm_ctx_default_cache_dir(g_st.data_dir, g_st.cache_dir, sizeof g_st.cache_dir);
appstate_load_settings();
g_st.logged_in = (g_st.ctx.cookie[0] != 0);
g_st.active_playlist = -1;
g_st.playing_song = -1;
g_st.qr_size = 0;
g_st.login_state = NCM_LOGIN_PENDING;
/* uid/nickname 从配置恢复(登录时落盘);uid 缺失时不在此阻塞校验——由 home_reload 拉歌单时
* appstate_ensure_uid 触发 login_status_check(启动快,避免首屏长时间网络阻塞)。 */
meow_config_get("ncm.uid", g_st.uid, sizeof g_st.uid);
meow_config_get("ncm.nickname", g_st.nickname, sizeof g_st.nickname);
return 0;
}
void appstate_load_settings(void)
{
g_st.cache_limit_gb = meow_config_int("ncm.cache_limit_gb", 10);
if (g_st.cache_limit_gb < 1) g_st.cache_limit_gb = 1;
if (g_st.cache_limit_gb > 100) g_st.cache_limit_gb = 100;
g_st.level = meow_config_int("ncm.level", NCM_LEVEL_HIRES);
if (g_st.level < NCM_LEVEL_STANDARD || g_st.level > NCM_LEVEL_HIRES)
g_st.level = NCM_LEVEL_HIRES;
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
}
void appstate_save_settings(void)
{
char b[16];
snprintf(b, sizeof b, "%d", g_st.cache_limit_gb);
meow_config_set("ncm.cache_limit_gb", b);
snprintf(b, sizeof b, "%d", g_st.level);
meow_config_set("ncm.level", b);
ncm_cache_set_limit_gb(&g_st.ctx.cache, g_st.cache_limit_gb);
g_st.ctx.default_level = g_st.level;
/* 手动调低上限时立即按 LRU 淘汰(超上限删最旧);未超限则为无操作 */
ncm_cache_evict(&g_st.ctx.cache);
}
void appstate_set_logged_in(const char *uid, const char *nickname)
{
if (uid && uid[0]) { ncm_strlcpy(g_st.uid, uid, sizeof g_st.uid); meow_config_set("ncm.uid", uid); }
if (nickname && nickname[0]) {
ncm_strlcpy(g_st.nickname, nickname, sizeof g_st.nickname);
meow_config_set("ncm.nickname", nickname);
}
g_st.logged_in = 1;
}
void appstate_logout(void)
{
ncm_ctx_clear_cookie(&g_st.ctx);
g_st.logged_in = 0;
g_st.uid[0] = 0;
g_st.nickname[0] = 0;
g_st.active_playlist = -1;
g_st.playing_song = -1;
g_st.song_count = 0;
meow_config_remove("ncm.uid");
meow_config_remove("ncm.nickname");
cover_cache_reset();
}
/* 若 uid 缺失(cookie 在但没存 uid),用 login_status_check 校验并取回。返回 0=成功。 */
int appstate_ensure_uid(void)
{
if (!g_st.logged_in || !g_st.ctx.cookie[0]) return NCM_ERR_AUTH;
if (g_st.uid[0]) return NCM_OK;
char uid[32], nick[128];
int rc = ncm_login_status_check(g_st.ctx.device_id, g_st.ctx.cookie, uid, sizeof uid,
nick, sizeof nick);
if (rc == NCM_OK) {
appstate_set_logged_in(uid, nick);
return NCM_OK;
}
/* 会话失效:清 cookie → 未登录 */
if (rc == NCM_ERR_AUTH) {
ncm_ctx_clear_cookie(&g_st.ctx);
g_st.logged_in = 0;
}
return rc;
}
int appstate_load_playlists(void)
{
if (!g_st.logged_in) return NCM_ERR_AUTH;
int rc = appstate_ensure_uid();
if (rc != NCM_OK) return rc;
memset(g_st.playlists, 0, sizeof g_st.playlists);
int n = 0;
rc = ncm_api_user_playlist(g_st.ctx.device_id, g_st.ctx.cookie, g_st.uid,
NCM_PLAYLIST_MAX, 0, g_st.playlists, NCM_PLAYLIST_MAX, &n);
g_st.playlist_count = (rc == NCM_OK) ? n : 0;
meow_log(MEOW_LOG_INFO, "home: playlists rc=%d count=%d uid=%s", rc, g_st.playlist_count, g_st.uid);
return rc;
}
int appstate_load_playlist_songs_page(int idx, int offset, int limit)
{
if (idx < 0 || idx >= g_st.playlist_count) return NCM_ERR_ARG;
/* 不在此处写 g_st.active_playlist:调用方(playlist_open/UI)已在 UI 线程设定;
* 若后台 worker 在此改写,会覆盖用户已切换到的新歌单,造成“打开 B 却显示 A”。 */
const ncm_playlist_t *pl = &g_st.playlists[idx];
if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH;
if (offset < 0) offset = 0;
/* REPORT §13.1:/api/playlist/track/all 服务端分页端点真机全 404(NCM 无此路由),弃用。
* 回到 §10.11 已验证路径并做**真分页**:detail(n=1)取【全量】trackIds 入 song_ids[],
* song_detail 按 ≤NCM_SONG_BATCH 分批拉歌曲对象(含 al.picUrl),offset 为「下一页」起始;
* 列表触底驱动 offset 前进,song_count 逐页累加(不再一次性拉满,大歌单不冻 UI,
* 也不把「第 N 页之后的歌」挡在 256 之外——NCM_SONG_MAX 上限提升到 1000,见 §14)。 */
if (offset == 0) {
/* 重头:重建 trackIds 索引 + 清空已物化歌曲(song_count 无论成败都归零)。 */
memset(g_st.song_ids, 0, sizeof g_st.song_ids);
memset(g_st.songs, 0, sizeof g_st.songs);
g_st.song_count = 0;
g_st.track_ids_count = 0;
int idn = 0;
int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id,
NULL, 0, NULL, 0,
g_st.song_ids, NCM_SONG_MAX, &idn);
if (rc != NCM_OK) return rc;
g_st.track_ids_count = idn;
/* 首屏只需第 0 页:song_detail 只拉前 limit 首,其余待滚动触底再拉(分页驱动)。 */
} else if (g_st.track_ids_count == 0) {
/* 防御:offset>0 但没有索引(不应发生;主路径先走 offset=0)。 */
return NCM_ERR_ARG;
}
if (offset >= g_st.track_ids_count) return NCM_OK; /* 已到尾部/上限 */
if (offset >= NCM_SONG_MAX) return NCM_OK; /* 内存上限:不再物化更多 */
int can = g_st.track_ids_count - offset;
int room = NCM_SONG_MAX - offset;
if (can > limit) can = limit; /* 每页 ≤ limit(保守 100) */
if (can > room) can = room; /* 不越界 song_ids[]/songs[] */
int bn = 0;
int rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[offset],
can, &g_st.songs[offset], NCM_SONG_MAX - offset, &bn);
if (rc != NCM_OK) return rc; /* 失败:song_count 不变(保留已载页) */
g_st.song_count = offset + bn; /* 成功:累加本页 */
appstate_apply_playlist_cover(idx);
return NCM_OK;
}
int appstate_load_playlist_songs(int idx)
{
/* 兼容入口:整批载入(内部按页循环到 NCM_SONG_MAX 或歌单尾部)。 */
int rc = appstate_load_playlist_songs_page(idx, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc;
int off = g_st.song_count;
while (off < g_st.track_ids_count && off < NCM_SONG_MAX) {
rc = appstate_load_playlist_songs_page(idx, off, NCM_SONG_BATCH);
if (rc != NCM_OK) return rc;
int nxt = g_st.song_count;
if (nxt <= off) break; /* 无进展:防死循环 */
off = nxt;
}
return NCM_OK;
}
void appstate_apply_playlist_cover(int idx)
{
if (idx < 0 || idx >= g_st.playlist_count) return;
ncm_strlcpy(g_st.cover_url, g_st.playlists[idx].cover, sizeof g_st.cover_url);
}
/* 设置播放页当前歌曲 + 封面 URL(player 页 UI 读)。idx=-1 表示清除。 */
void appstate_set_playing_song(int idx, const char *cover_url)
{
g_st.playing_song = idx;
if (cover_url && cover_url[0])
ncm_strlcpy(g_st.cover_url, cover_url, sizeof g_st.cover_url);
}
int appstate_current_playing(void)
{
if (g_st.playing_song < 0 || g_st.playing_song >= g_st.song_count) return -1;
return g_st.playing_song;
}
/* ---- 真机联调 autoplay hook(A2 音频迁移验收用,非正式能力)----
* 触发文件 /tmp/ncm_autoplay 存在且 home 歌单已载 → 后台线程找「已缓存」曲目并播放:
* - 内容为纯数字 → 从该歌单下标起扫最多 AUTOPLAY_SCAN_PL 个歌单首页;
* - 内容为其它文本 → 按歌单名子串匹配**单个**歌单(如 "Docu";避开超大歌单的
* 全量 trackIds 解析,真机联调省时省内存);
* - 空文件 → 从 0 起扫。
* 风控纪律:**绝不发 song_url 下载**——无缓存命中只记日志退出;metadata 请求 = 每歌单
* 1 detail + 1 song_detail(≤ 2×AUTOPLAY_SCAN_PL 次)。 */
#define AUTOPLAY_TRIGGER "/tmp/ncm_autoplay"
#define AUTOPLAY_SCAN_PL 3
/* 播放单个歌单首页中第一首已缓存曲目。命中播放返回 1,否则 0。 */
static int autoplay_try_playlist(int pl)
{
int rc = appstate_load_playlist_songs_page(pl, 0, NCM_SONG_BATCH);
if (rc != NCM_OK) {
meow_log(MEOW_LOG_WARN, "autoplay: playlist[%d] load rc=%d", pl, rc);
return 0;
}
char cpath[600];
for (int i = 0; i < g_st.song_count; i++) {
if (ncm_cache_lookup(&g_st.ctx.cache, g_st.songs[i].id, ".mp3",
cpath, sizeof cpath) == NCM_OK) {
g_st.active_playlist = pl;
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play",
pl, i, g_st.songs[i].id);
player_manager_play(i);
return 1;
}
}
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first page %d songs, no cached track",
pl, g_st.song_count);
return 0;
}
static void *autoplay_worker(void *arg)
{
const char *sel = (const char *)arg; /* NULL=从 0 起扫;数字=起始下标;其它=歌单名子串 */
if (sel && sel[0] && !(sel[0] >= '0' && sel[0] <= '9')) {
for (int pl = 0; pl < g_st.playlist_count && !g_app_quit; pl++) {
if (strstr(g_st.playlists[pl].name, sel)) {
meow_log(MEOW_LOG_INFO, "autoplay: name '%s' -> playlist[%d] '%s' (%d tracks)",
sel, pl, g_st.playlists[pl].name, g_st.playlists[pl].track_count);
if (autoplay_try_playlist(pl)) { free((void *)sel); return NULL; }
break;
}
}
meow_log(MEOW_LOG_WARN, "autoplay: no playlist name match / no cached track ('%s')", sel);
free((void *)sel);
return NULL;
}
int start = (sel && sel[0]) ? atoi(sel) : 0;
free((void *)sel);
if (start < 0) start = 0;
int end = start + AUTOPLAY_SCAN_PL;
if (end > g_st.playlist_count) end = g_st.playlist_count;
for (int pl = start; pl < end && !g_app_quit; pl++)
if (autoplay_try_playlist(pl)) return NULL;
meow_log(MEOW_LOG_WARN, "autoplay: no cached track found (no download, risk control)");
return NULL;
}
void autoplay_maybe(void)
{
static int s_fired = 0;
if (s_fired || g_app_quit) return;
if (access(AUTOPLAY_TRIGGER, F_OK) != 0) return;
if (!g_st.logged_in || g_st.playlist_count <= 0) return; /* 等 home 歌单载完 */
s_fired = 1;
char buf[64] = "";
FILE *f = fopen(AUTOPLAY_TRIGGER, "r"); /* 一次性:读完即删 */
if (f) {
if (fgets(buf, sizeof buf, f)) {
size_t n = strlen(buf);
while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0;
}
fclose(f);
}
unlink(AUTOPLAY_TRIGGER);
meow_log(MEOW_LOG_INFO, "autoplay: triggered, sel='%s'", buf);
char *sel = strdup(buf);
pthread_t t;
if (pthread_create(&t, NULL, autoplay_worker, sel) == 0) pthread_detach(t);
else free(sel);
}