- 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(已热更设备)
295 lines
12 KiB
C
295 lines
12 KiB
C
/* 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);
|
||
}
|