feat: M6.10–M6.19 批量收编 + M6.19 推卡验收

- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘)
- OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复
- ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640
- 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫)
- 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
This commit is contained in:
AskaEth committed 2026-09-03 12:16:01 +08:00
1 parent c2ee7626a4
commit 3d70a3dd2c
176 files changed
+15507 -597

No files matched your search

+111 -1
View File
@@ -22,13 +22,15 @@
* (无声排查的永久资产,勿省)。
*/
#include "../ncm_app_internal.h"
#include "meow_ipc.h" /* AUDIO_FMT_S16 */
#include "meow_ipc.h" /* AUDIO_FMT_S16 / MSG_AUDIO_VOLUME / MEOW_IPC_HDR_LEN */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#ifdef __linux__
#include <sound/asound.h>
@@ -143,6 +145,111 @@ static int file_open(int rate, int ch, int bits)
return 0;
}
/* ============================================================ */
/* 任务1:开播前把「系统当前音量」同步下发到 audiod */
/* ============================================================ */
/* 背景(用户实测 2026-09-02):未开始播放时音量不是系统设定值;按一下音量键才跳到设定值。
* 根因= ncm 启动并打开 audiod/DAC 时,没有把系统当前音量下发到 audiod(meow_audiod 只在
* MSG_AUDIO_VOLUME 收到时才写 DAC,流开/重配上不主动应用 settings 音量),导致 DAC 停在
* 默认/上次残留音量;用户按音量键时 launcher/watchdog 那条音量链才更新并下发到 audiod。
* 修法= SINK_AUDIOD open 成功后读 settings.conf 的 audio.volume,经 MSG_AUDIO_VOLUME 下发
* (audiod 写 DAC numid=1/2 + 持久化 settings,同源同键)。语意:CS43198 寄存器=衰减量
* (0 最响/255 静音),audiod dac_vol_set 负责反转与真静音(写 255),本处只传 pct。
* 宿主验证:fake audiod socket 收帧断言 pct==系统设定(bench/vol_sync_sim.c)。 */
#define NCM_SETTINGS_CONF "/usr/data/meow/settings.conf" /* 与 audiod SETTINGS_CONF 同源 */
#define NCM_AUDIO_SOCK_DEF "/usr/data/meow_audio.sock" /* 与 audiod / SDK 客户端同缺省 */
#define NCM_VOL_DEFAULT 60 /* 与 audiod VOL_DEFAULT/SDK 缺省一致 */
#define NCM_VOL_MAX 100
static const char *settings_conf_path(void)
{
const char *e = getenv("MEOW_SETTINGS_FILE"); /* 宿主单测指向临时文件;设备缺省系统 conf */
return (e && e[0]) ? e : NCM_SETTINGS_CONF;
}
/* 读 settings.conf 的 audio.volume(扁平 "key = value";兼容无空格/多空格)。
* 与 launcher meow_settings / SDK meow_ui_vol_read 同款解析。缺省 60;夹到 [0,100]。 */
static int settings_read_volume(void)
{
static const char key[] = "audio.volume";
const int kl = (int)sizeof key - 1;
FILE *f = fopen(settings_conf_path(), "r");
int vol = NCM_VOL_DEFAULT;
if (!f) return vol;
char line[160];
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue; /* 非 key=value 行跳过 */
char *k = line;
while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */
int n = (int)(eq - k);
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; /* key 右去空白 */
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* 精确匹配 */
char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++; /* value 左去空白 */
int x = atoi(v);
vol = (x < 0) ? 0 : (x > NCM_VOL_MAX ? NCM_VOL_MAX : x);
break;
}
fclose(f);
return vol;
}
/* 把 pct(0-100) 下发到 audiod。独立连接(SDK 客户端连接私有不可复用);MSG_AUDIO_VOLUME
* 不要求开流,audiod 侧写 DAC+持久化后 ACK(meow_audiod.c MSG_AUDIO_VOLUME)。失败仅日志,
* 不影响播放本身(音量是次要正确性,非播放成败)。
*
* §2026-09-02 连接风暴根因:audiod_sync_volume 每次 audio_out_open 都新建一条 AF_UNIX
* 短连接(socket→connect→write→close)。边下边播/切歌/seek 反复 open/close 时,audiod 端表现为
* 「同一 fd=N 被反复 accept」(真机 1489 次 "io: client connected (fd=4)")。音量值实际只在
* 用户按音量键时变化,且 audiod 收到已持久化(settings_set_volume),无需每次 open 重发——
* 修法:每进程首次 open 同步一次;此后仅当 settings 音量**变化**才重发(同一连接数坍缩为
* O(1),风暴消除)。失败不记 status(下次 open 仍会尝试,幂等)。 */
static void audiod_sync_volume(int pct)
{
const char *sp = getenv("MEOW_AUDIO_SOCK");
if (!sp || !sp[0]) sp = NCM_AUDIO_SOCK_DEF;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, sp, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return; }
uint8_t frame[MEOW_IPC_HDR_LEN + sizeof(int32_t)];
uint32_t magic = MEOW_IPC_MAGIC, len = sizeof(int32_t), req = 1;
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_VOLUME;
memcpy(frame + 0, &magic, 4);
memcpy(frame + 4, &ver, 2);
memcpy(frame + 6, &type, 2);
memcpy(frame + 8, &req, 4);
memcpy(frame + 12, &len, 4);
int32_t v32 = (int32_t)pct;
memcpy(frame + 16, &v32, sizeof v32);
if (write(fd, frame, sizeof frame) != (ssize_t)sizeof frame)
meow_log(MEOW_LOG_WARN, "audio: sync volume write failed pct=%d", pct);
close(fd);
}
/* 每进程同步一次 + 值变才重发:杜绝每次 audio_out_open 新建短连接(fd=N accept 风暴根因)。
* g_vol_synced=1 即已同步过;g_vol_last=上次下发的 pct,二者相同则零连接直接返回。 */
static int g_vol_synced = 0;
static int g_vol_last = -1;
int audio_out_sync_volume(void)
{
int pct = settings_read_volume();
if (g_vol_synced && pct == g_vol_last) return 0; /* 已同步且未变:零新建连接 */
g_vol_synced = 1;
g_vol_last = pct;
audiod_sync_volume(pct);
meow_log(MEOW_LOG_INFO, "audio: sync volume -> audiod pct=%d (settings %s)",
pct, settings_conf_path());
return 0;
}
int audio_out_open(int sample_rate, int channels, int bits)
{
g_rate = sample_rate; g_ch = channels; g_bits = bits;
@@ -202,6 +309,9 @@ int audio_out_open(int sample_rate, int channels, int bits)
else
meow_log(MEOW_LOG_INFO, "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit",
sid, sample_rate, channels, bits);
/* 任务1:开播/打开音频会话时把系统当前音量同步下发到 audiod(DAC 停在默认/上次残留音量的根治)。
* 幂等(同值重写无害);失败不阻断播放。 */
audio_out_sync_volume();
return 0;
}
+78
View File
@@ -21,6 +21,7 @@
static pthread_t g_thr;
static int g_thr_on;
static int g_quit;
static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */
static const char *sock_path(void)
{
@@ -151,6 +152,11 @@ static void apply_playctl(const m_playctl_t *c)
* (停止/缓冲)时 pause() 内部按态过滤,天然 no-op 安全。 */
player_manager_pause(player_manager_state() == NCM_PC_PAUSED ? 0 : 1);
break;
case NCM_CTL_FOCUS:
/* 任务3:通知中心媒体卡片点「卡片体」→ launcher 发本 cmd(需要 launcher 接线,见任务书):
* 请求 App 回到播放页。只置标志(跨线程),UI 线程 app_on_tick 消费后导航(page nav 非线程安全)。 */
g_req_focus_player = 1;
break;
default: break;
}
}
@@ -201,6 +207,78 @@ static void *ipc_thread(void *arg)
return NULL;
}
/* 任务2:home 根页左滑「回桌面后台」——不再进程退出,改发 MSG_NAV{EXIT} 给 launcher。
* launcher M4 据 app.json background_audio + 媒体会话 PLAYING(meow_media_app_playing)判
* 保活(进程+音频流保留转后台,UI 走 §10.5 SDK present 自抑,非 SIGSTOP);否则才真终止。
* 本函数**不**置 g_app_quit、**不**停线程——进程保留,媒体会话继续上报 PLAYING(任务2 ③)。
* 独立连接发一帧(launcher ipc_dispatch MSG_NAV{EXIT} → meow_nav_request_exit,app_launcher.c:571)。
* 返回 0=发出 / -1=无 socket 或写失败。 */
int ipc_media_nav_exit(void)
{
int fd = connect_path(sock_path());
if (fd < 0) {
meow_log(MEOW_LOG_WARN, "ipc: nav_exit no launcher socket");
return -1;
}
m_nav_t n;
memset(&n, 0, sizeof n);
ncm_strlcpy(n.app_id, "dev.player.ncm", sizeof n.app_id);
n.nav_type = MEOW_NAV_EXIT;
n.depth = 0;
int rc = send_frame(fd, MSG_NAV, 9, &n, sizeof n);
close(fd);
meow_log(MEOW_LOG_INFO, "ipc: nav_exit sent rc=%d (request launcher background)", rc);
return rc;
}
/* 任务4 keep_awake:ACT_* 值 → launcher app.json awake/route 键用的活动名。
* 命中的只有这些(ncm_app_internal.h ACT_* 枚举);未知/空栈(0) 返回 NULL(不起作用)。 */
static const char *ncm_act_name(int act)
{
switch (act) {
case ACT_LOGIN: return "login";
case ACT_HOME: return "home";
case ACT_PLAYLIST: return "playlist";
case ACT_PLAYER: return "player";
case ACT_SETTINGS: return "settings";
case ACT_SEARCH: return "search";
default: return NULL;
}
}
/* 任务4 keep_awake:把当前顶层 activity 上报 launcher(MSG_ACT_FOCUS,独立连接发一帧,无需 HELLO)。
* launcher util.c ipc_dispatch MSG_ACT_FOCUS → meow_fg_act_set(app,act);fg_keep_awake() 依
* app.json awake.<act>=1 判定该 activity 是否常亮(超时不自动息屏)。无 socket/launcher 不跑 → 优雅跳过。
* 返回 0=发出 / -1=无 socket 或写失败。 */
int ipc_act_report(int act)
{
const char *name = ncm_act_name(act);
if (!name) return -1;
int fd = connect_path(sock_path());
if (fd < 0) {
meow_log(MEOW_LOG_WARN, "ipc: act_focus(%s) no launcher socket", name);
return -1;
}
m_act_focus_t f;
memset(&f, 0, sizeof f);
ncm_strlcpy(f.app_id, "dev.player.ncm", sizeof f.app_id);
ncm_strlcpy(f.activity, name, sizeof f.activity);
int rc = send_frame(fd, MSG_ACT_FOCUS, 10, &f, sizeof f);
close(fd);
meow_log(MEOW_LOG_INFO, "ipc: act_focus act=%s rc=%d", name, rc);
return rc;
}
/* 任务3:UI 线程消费「回到播放页」标志。返回 1=有未消费的聚焦请求(消费一次)。 */
int ipc_media_focus_consume(void)
{
if (g_req_focus_player) {
g_req_focus_player = 0;
return 1;
}
return 0;
}
int ipc_media_start(void)
{
if (g_thr_on) return 0;
+201
View File
@@ -0,0 +1,201 @@
/* play/mem_watch.c — 边播边监控**取证**实现(接口见 mem_watch.h;agent 32bit 大文件 OOM 取证)。
*
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / meow_svc_perf;
* 本模块退化为「RssAnon 取证」:低频读 /proc/self/status(优先 RssAnon,缺失回退 VmRSS),
* 记录匿名内存峰值,**仅供日志**。不再驱动下载/预取降载(那由 meow_cachegov_download_paused
* / prefetch_paused 查询)。**绝不打断解码/播放**。
*
* 阈值滞后(仅用于取证 level 标注):
* kb >= CRITICAL → level=2;kb >= HIGH 且当前<2 → level=1;kb <= LOW → level=0;其它保持。
* 本模块自含:只依赖 pthread + meow_log(SDK 日志),不依赖 core/app 其它 TU。
*/
#include "mem_watch.h"
#include "../sdk/include/meow_ui_log.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* 默认阈值(设备缺省;宿主单测用 env/force 桩覆盖) */
#define MEM_WATCH_PERIOD_MS 500
#define MEM_WATCH_HIGH_KB (30 * 1024)
#define MEM_WATCH_LOW_KB (20 * 1024)
#define MEM_WATCH_CRITICAL_KB (40 * 1024)
struct mw_cfg {
int period_ms;
long high_kb;
long low_kb;
long critical_kb;
};
static struct mw_cfg g_cfg = {
.period_ms = MEM_WATCH_PERIOD_MS,
.high_kb = MEM_WATCH_HIGH_KB,
.low_kb = MEM_WATCH_LOW_KB,
.critical_kb = MEM_WATCH_CRITICAL_KB,
};
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_t g_thr;
static int g_thr_on;
static volatile int g_quit;
static long g_rss_anon_kb; /* 最近一次采样(KB;-1=不可读) */
static long g_peak_song_kb; /* 本曲匿名峰值(song_begin 清零) */
static int g_level; /* NCM_MEM_LVL_*(仅取证日志) */
static long g_forced_kb = -1; /* 宿主桩 ≥0 时替代真读 */
/* env 覆盖阈值:非法值回落默认。 */
static int env_int(const char *k, int def)
{
const char *e = getenv(k);
if (!e || !e[0]) return def;
int v = atoi(e);
return v > 0 ? v : def;
}
static long env_long(const char *k, long def)
{
const char *e = getenv(k);
if (!e || !e[0]) return def;
long v = atol(e);
return v > 0 ? v : def;
}
/* 读匿名 RSS(KB)。优先 RssAnon;缺则回退 VmRSS;都无 → -1。 */
static long read_rss_kb(void)
{
pthread_mutex_lock(&g_mu);
long forced = g_forced_kb;
pthread_mutex_unlock(&g_mu);
if (forced >= 0) return forced;
FILE *fp = fopen("/proc/self/status", "r");
if (!fp) return -1;
char line[256];
long anon = -1, rss = -1;
while (fgets(line, sizeof line, fp))
if (!strncmp(line, "RssAnon:", 8)) anon = atol(line + 8);
else if (rss < 0 && !strncmp(line, "VmRSS:", 6)) rss = atol(line + 6);
fclose(fp);
return anon >= 0 ? anon : rss;
}
static const char *level_str(int lvl)
{
switch (lvl) {
case NCM_MEM_LVL_HIGH: return "high";
case NCM_MEM_LVL_CRITICAL: return "critical";
default: return "normal";
}
}
static void *watch_thread(void *arg)
{
(void)arg;
while (!g_quit) {
long kb = read_rss_kb();
int lvl;
pthread_mutex_lock(&g_mu);
g_rss_anon_kb = kb;
if (kb > g_peak_song_kb) g_peak_song_kb = kb;
long hi = g_cfg.high_kb, lo = g_cfg.low_kb, cr = g_cfg.critical_kb;
/* 滞后三档(仅取证 level 标注) */
if (kb >= cr) lvl = NCM_MEM_LVL_CRITICAL;
else if (kb >= hi) lvl = (g_level == NCM_MEM_LVL_CRITICAL) ? NCM_MEM_LVL_CRITICAL : NCM_MEM_LVL_HIGH;
else if (kb <= lo) lvl = NCM_MEM_LVL_NORMAL;
else lvl = g_level; /* 滞后带:保持 */
if (lvl != g_level) {
meow_log(MEOW_LOG_INFO,
"[mem] rss_anon=%ldKB peak=%ldKB level=%s (H=%ldK/L=%ldK/C=%ldK)",
kb, g_peak_song_kb, level_str(lvl), hi, lo, cr);
}
g_level = lvl;
pthread_mutex_unlock(&g_mu);
usleep((useconds_t)g_cfg.period_ms * 1000u);
}
return NULL;
}
int mem_watch_start(void)
{
pthread_mutex_lock(&g_mu);
if (g_thr_on) { pthread_mutex_unlock(&g_mu); return 0; }
/* 开关(默认开;NCM_MEM_WATCH_ENABLED=0/off 关闭监控,行为退化为旧版零侵入) */
const char *en = getenv("NCM_MEM_WATCH_ENABLED");
if (en && (!en[0] || !strcmp(en, "0") || !strcmp(en, "off") || !strcmp(en, "false"))) {
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[mem] watch disabled (NCM_MEM_WATCH_ENABLED=%s)", en);
return 0;
}
/* 阈值 env 覆盖(宿主单测) */
g_cfg.period_ms = env_int("NCM_MEM_WATCH_PERIOD_MS", MEM_WATCH_PERIOD_MS);
g_cfg.high_kb = env_long("NCM_MEM_WATCH_HIGH_KB", MEM_WATCH_HIGH_KB);
g_cfg.low_kb = env_long("NCM_MEM_WATCH_LOW_KB", MEM_WATCH_LOW_KB);
g_cfg.critical_kb = env_long("NCM_MEM_WATCH_CRITICAL_KB", MEM_WATCH_CRITICAL_KB);
g_quit = 0;
pthread_attr_t a;
pthread_attr_init(&a);
pthread_attr_setstacksize(&a, (size_t)(128 * 1024)); /* 同 ncm_thread_create 128KB(内存瘦身,OOM 修复) */
int rc = pthread_create(&g_thr, &a, watch_thread, NULL);
pthread_attr_destroy(&a);
if (rc != 0) { pthread_mutex_unlock(&g_mu); return -1; }
g_thr_on = 1;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO,
"[mem] watch started period=%dms H=%ldK L=%ldK C=%ldK (RssAnon 优先)",
g_cfg.period_ms, g_cfg.high_kb, g_cfg.low_kb, g_cfg.critical_kb);
return 0;
}
void mem_watch_stop(void)
{
g_quit = 1;
pthread_mutex_lock(&g_mu);
if (g_thr_on) { g_thr_on = 0; pthread_detach(g_thr); }
pthread_mutex_unlock(&g_mu);
}
int mem_watch_level(void)
{
pthread_mutex_lock(&g_mu);
int v = g_level;
pthread_mutex_unlock(&g_mu);
return v;
}
long mem_watch_rss_anon_kb(void)
{
pthread_mutex_lock(&g_mu);
long v = g_rss_anon_kb;
pthread_mutex_unlock(&g_mu);
return v;
}
void mem_watch_song_begin(void)
{
long kb = read_rss_kb(); /* 起播即采样当前内存,本曲峰值从当前起算 */
pthread_mutex_lock(&g_mu);
g_peak_song_kb = kb;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[mem] song begin rss_anon=%ldKB", kb);
}
/* 播完/切歌释放钩子之后调用:落本曲峰值 + 重置计数。 */
void mem_watch_song_end(void)
{
pthread_mutex_lock(&g_mu);
long peak = g_peak_song_kb;
g_peak_song_kb = g_rss_anon_kb;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[mem] song end peak_anon=%ldKB (released)", peak);
}
void mem_watch_set_forced_kb(long kb)
{
pthread_mutex_lock(&g_mu);
g_forced_kb = kb;
pthread_mutex_unlock(&g_mu);
}
+55
View File
@@ -0,0 +1,55 @@
/* play/mem_watch.h — 边播边监控**取证**(agent 32bit 大文件 OOM 取证;OOM_PAGECACHE_GOVERNANCE)。
*
* 归属演进(2026-09-02 定案,ARCHITECTURE_PRINCIPLES ③混合):**降载决策上收通用层**——
* A(DONTNEED 治理)+ C(/proc/meminfo Cached 监控)分别归 SDK meow_cachegov / 系统服务
* meow_svc_perf;本模块退化为「RssAnon 取样取证」:一个轻量线程(ncm_thread_create 同款
* 128KB 栈)低频(默认 500ms)读 /proc/self/status 的 RssAnon(缺失回退 VmRSS),记录匿名
* 内存峰值(RssAnon 贴近「应用自有不可回收内存」,是 OOM kill 高危口径),**仅供日志/取证**,
* 不再驱动下载/预取降载(那由 meow_cachegov_download_paused/prefetch_paused 查询)。
*
* 调度器结合释放钩子:mem_watch_song_begin/end 在 run_song 的起播/收尾(song_src_dispose +
* ncm_dec_close + audio_out_close,即「彻底释放」钩子)处调用,重置本曲峰值计数并落日志
* (峰值 RSS 即 RssAnon/RssFile 分离取证锚点;Cached/Dirty 侧取证归 svc_perf/meow_cachegov)。
*
* 阈值/开关可通过环境变量覆盖(宿主单测确定性驱动):NCM_MEM_WATCH_ENABLED /
* NCM_MEM_WATCH_PERIOD_MS / NCM_MEM_WATCH_HIGH_KB / NCM_MEM_WATCH_LOW_KB / NCM_MEM_WATCH_CRITICAL_KB。
* 宿主测试桩:mem_watch_set_forced_kb() 注入读值(>=0 时监控线程不再读真 proc,仅测取证逻辑)。
*/
#ifndef NCM_MEM_WATCH_H
#define NCM_MEM_WATCH_H
#ifdef __cplusplus
extern "C" {
#endif
/* 内存压力档(仅取证日志用;不再驱动降载) */
enum {
NCM_MEM_LVL_NORMAL = 0, /* < HIGH */
NCM_MEM_LVL_HIGH = 1, /* ≥ HIGH */
NCM_MEM_LVL_CRITICAL = 2, /* ≥ CRITICAL */
};
/* 启动监控线程(幂等;ncm_thread_create 128KB 栈)。返回 0=ok / -1=线程建失败。 */
int mem_watch_start(void);
/* 停止(置退出标志;线程 detach,不 join——同 ipc_media 模式)。 */
void mem_watch_stop(void);
/* 当前压力档(0/1/2;仅取证日志)。 */
int mem_watch_level(void);
/* 最近一次采样的匿名 RSS(KB);-1=不可读。 */
long mem_watch_rss_anon_kb(void);
/* ---- 调度器:与播放释放钩子联动 ---- */
/* 起播一首曲时调用:本曲峰值计数清零,开始记录本曲内存画像。 */
void mem_watch_song_begin(void);
/* 播完/切歌、即「彻底释放」钩子(decoder 关+下载线程 join+源关+.part 收尾)执行后调用:
* 落本曲峰值日志 + 重置计数,给下一首让出统计(内存本身已由释放钩子归还)。 */
void mem_watch_song_end(void);
/* ---- 宿主测试桩(设备永不调用;≥0 时监控线程用注入值替代真 /proc 读)---- */
void mem_watch_set_forced_kb(long kb);
#ifdef __cplusplus
}
#endif
#endif /* NCM_MEM_WATCH_H */
+84 -2
View File
@@ -32,6 +32,7 @@
#include <sys/stat.h>
#include "decoder.h"
#include "stream_src.h"
#include "mem_watch.h"
static pthread_t g_thr;
static int g_thr_on;
@@ -43,6 +44,10 @@ static char g_path[512]; /* 本地音频路径(缓存,mp3/flac
static int64_t g_file_bytes;
static int g_level; /* 音质档(来自设置) */
static int g_cache_gb;
/* 任务3(§re-entry):pending_replay=1 表示「切音质/缓存后点了正在播的同曲」——此时重播须换档
* (P12 语义,seq++),不得被「同曲不重播」护栏吞掉。set_quality 置位;player_manager_play 的
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
static int g_pending_replay;
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
@@ -90,12 +95,25 @@ void player_manager_set_quality(int level, int cache_gb)
pthread_mutex_lock(&g_play.mu);
g_play.err = 0;
ncm_cache_set_limit_gb(&g_st.ctx.cache, cache_gb);
g_pending_replay = 1; /* 任务3:改档后点正在播的同曲 → 强制换档重播(§25 P12) */
pthread_mutex_unlock(&g_play.mu);
}
void player_manager_play(int song_idx)
{
pthread_mutex_lock(&g_play.mu);
/* 任务3:点「正在播的曲」(同 idx 且 PLAYING/PAUSED,非切档重播)→ 只聚焦不重播:
* 不 seq++、不置 BUFFER——旧曲继续出声/停原状态,UI 由调用方 nav 到播放页。
* 注意:set_quality 已置 g_pending_replay=1(切音质后重播同曲须换档,P12),此护栏放行。
* 非 PLAYING/PAUSED(STOPPED/另一曲)不受影响,照旧起播。 */
if (song_idx == g_play.cur_song && !g_pending_replay &&
(g_play.state == NCM_PC_PLAYING || g_play.state == NCM_PC_PAUSED)) {
pthread_mutex_unlock(&g_play.mu);
meow_log(MEOW_LOG_INFO, "play: re-enter current idx=%d state=%d -> no restart",
song_idx, g_play.state);
return;
}
g_pending_replay = 0; /* 消费掉(新播放意图落地,后续同曲 tap 恢复「只聚焦」) */
g_play.cur_song = song_idx;
g_play.cmd = 0;
g_play.state = NCM_PC_BUFFER;
@@ -108,6 +126,7 @@ void player_manager_play(int song_idx)
g_play.underruns = 0; /* §21:边下边播统计随新播放意图清零 */
g_play.underrun_ms = 0;
g_play.stalled = 0;
g_play.refaults = 0; /* §2.3:refault 取证随新播放意图清零 */
pthread_mutex_unlock(&g_play.mu);
}
@@ -135,6 +154,7 @@ void player_manager_next(int dir)
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
if (c >= cnt) c = cnt - 1;
if (c != g_play.cur_song) {
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
g_play.cur_song = c;
g_play.cmd = 0;
g_play.state = NCM_PC_BUFFER;
@@ -147,6 +167,7 @@ void player_manager_next(int dir)
g_play.underruns = 0;
g_play.underrun_ms = 0;
g_play.stalled = 0;
g_play.refaults = 0;
}
pthread_mutex_unlock(&g_play.mu);
}
@@ -154,6 +175,7 @@ void player_manager_next(int dir)
void player_manager_stop(void)
{
pthread_mutex_lock(&g_play.mu);
g_pending_replay = 0; /* 停止:清「切档重播」待办 */
g_play.cur_song = -1;
g_play.state = NCM_PC_STOPPED;
g_play.pos_ms = 0;
@@ -189,8 +211,19 @@ typedef struct {
int *st_ptr; /* 指向 dl_thread 的 status 局部变量(同线程,无竞态) */
int need_fresh; /* 416/其它:服务器拒绝续传 → 放 .part、交调用方整首重下 */
uint64_t skip; /* 200 忽略 Range:body 前 skip 字节与 .part 前缀重复,丢弃 */
/* page-cache 治理(A:meow_cachegov)登记路径:播放实际读的文件(流式=.tmp/.part,命中=final)。 */
char gov_path[512];
/* 曲终 L0 用:commit 后的正式路径(流式 commit rename 后 = final;命中=gov_path 同)。 */
char gov_final_path[512];
} song_src_t;
/* page-cache 治理(A):SDK meow_cachegov 的 read_pos 回调——返回当前解码逻辑读指针
* (stream_src 内部维护,经 stream_src_read_pos 读取;回调上下文=stream_src*)。 */
static uint64_t gov_read_pos(void *ctx)
{
return stream_src_read_pos((stream_src_t *)ctx);
}
/* http 体回调(下载线程上下文;ncm_http 每次投递 ≤4KB):write(2) 直写落盘 +
* 锁内推进 written + broadcast 唤醒读端 + 进度反馈。
* 阶段3 续传(§5.2):首 body 字节按 response status 切换写姿态——
@@ -201,6 +234,11 @@ static int stream_dl_cb(void *ctx, const unsigned char *data, size_t len)
{
song_src_t *ss = (song_src_t *)ctx;
if (stream_src_aborted(ss->src)) return 1; /* 切歌/停止:中止 http */
/* page-cache 治理(§2.3/§3):下载 cb 查 meow_cachegov_download_paused()(A 回退硬暂停
* OR svc_perf C Cached 降载)→ 暂停下载(限速),优先解码播放;不打断播放线程
* (读端追上下载走既有 underrun/停滞联动)。每 50ms 查一次中止,切歌可立即脱身。 */
while (meow_cachegov_download_paused() && !stream_src_aborted(ss->src))
usleep(50 * 1000);
if (ss->first_body) {
ss->first_body = 0;
int st = ss->st_ptr ? *ss->st_ptr : 0;
@@ -295,6 +333,15 @@ static void song_src_dispose(song_src_t *ss)
if (ss->src) { stream_src_close(ss->src); ss->src = NULL; }
}
/* page-cache 治理(A)曲终清理:注销 L1 登记条目 + 下载完整(commit 成功)则对正式文件做
* L0 整文件 DONTNEED(仅曲终安全窗口;未 commit/中断保留 .part 不丢)。 */
static void song_gov_cleanup(song_src_t *ss)
{
if (ss->gov_path[0]) meow_cachegov_unregister(ss->gov_path);
if (ss->dl_rc == NCM_OK && ss->gov_final_path[0])
meow_cachegov_drop_finished(ss->gov_final_path);
}
/* 缓存键(用户定案 NCM_CACHE_DESIGN §2.5):歌曲ID + 音质档位 → "song_<id>_<level>.<ext>"。
* 同歌同档命中不重复下载;换档=新条目,旧档留给 LRU 自然淘汰。
* §20:键后缀用 ncm_api_level_key(sky 与 sky_c51 可区分)。 */
@@ -587,6 +634,7 @@ static void run_song(int idx)
g_play.play_song = s;
g_play.play_song_valid = 1;
pthread_mutex_unlock(&g_play.mu);
mem_watch_song_begin(); /* 边播边监控:本曲峰值从当前起算(下载/解码/播放全程记录) */
song_src_t ss;
int64_t file_bytes = 0;
@@ -602,11 +650,22 @@ static void run_song(int idx)
}
if (!still_intent(idx, my_seq)) { /* 预缓冲完成但已切歌/同曲重播:勿开解码/勿动状态 */
song_src_dispose(&ss);
song_gov_cleanup(&ss); /* 未登记(register 在其后)时为 no-op,统一清理 */
mem_watch_song_end(); /* 释放钩子:join 下载线程+关源+缓冲 free+.part 收尾 */
return;
}
ncm_strlcpy(g_path, path, sizeof g_path);
g_file_bytes = file_bytes;
/* page-cache 治理(A):登记本曲缓存文件(播放实际读的路径)+ read_pos 回调,供 L1 丢已读
* 前缀 / refault 回退。cache 命中(ss.w==NULL)path=正式文件;流式 path=.tmp/.part
* (cur file 即正被 pread 的文件,DONTNEED 只作用于它;正式路径待 commit 后 L0 用)。 */
ncm_strlcpy(ss.gov_path, path, sizeof ss.gov_path);
if (ss.w) ncm_strlcpy(ss.gov_final_path, ncm_cache_writer_final_path(ss.w), sizeof ss.gov_final_path);
else ncm_strlcpy(ss.gov_final_path, path, sizeof ss.gov_final_path);
meow_cachegov_register(ss.gov_path, (uint64_t)(file_bytes > 0 ? file_bytes : 0),
gov_read_pos, ss.src);
int64_t dur = s.duration_ms > 0 ? s.duration_ms : 0;
/* 解码分发(§19/§21):stream_src 上 magic 探测 fLaC→dr_flac / 其它→minimp3(open_cb
@@ -616,6 +675,8 @@ static void run_song(int idx)
int64_t total_frames = 0;
if (ncm_dec_open_src(&dec, ss.src, &rate, &ch, &bits, &total_frames) != 0) {
song_src_dispose(&ss);
song_gov_cleanup(&ss);
mem_watch_song_end(); /* 释放钩子 */
if (!still_intent(idx, my_seq)) return; /* open 期间(flac 元数据等待)已切歌/重播 */
set_err(NCM_ERR_IO, "decode open failed"); return;
}
@@ -623,7 +684,8 @@ static void run_song(int idx)
/* 音频输出:按实际流参数开(flac ≤16bit→S16,>16bit→S32;audiod 侧格式归一+原生率跟随) */
if (audio_out_open(rate, ch, bits) != 0) {
ncm_dec_close(dec); song_src_dispose(&ss);
ncm_dec_close(dec); song_src_dispose(&ss); song_gov_cleanup(&ss);
mem_watch_song_end(); /* 释放钩子 */
set_err(NCM_ERR_IO, "audio open failed"); return;
}
@@ -645,6 +707,7 @@ static void run_song(int idx)
int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */
int stalled_edge = 0; /* §21 停滞沿:已对 audiod pause(1),待数据恢复 pause(0) */
int underruns_seen = 0;
int refaults_seen = 0; /* §2.3:本曲 DONTNEED 丢页重读 TF 计数(取证) */
for (;;) {
if (g_app_quit) break;
@@ -751,6 +814,18 @@ static void run_song(int idx)
idx, u, (unsigned long long)wm);
}
}
/* page-cache 治理(A):refault 取证落 g_play + 节拍执行 L1 丢弃/refault 回退 */
{
int rf = stream_src_refaults(ss.src);
if (rf != refaults_seen) {
refaults_seen = rf;
pthread_mutex_lock(&g_play.mu);
g_play.refaults = rf;
pthread_mutex_unlock(&g_play.mu);
meow_log(MEOW_LOG_INFO, "play: refault idx=%d count=%d", idx, rf);
}
meow_cachegov_tick();
}
/* 更新进度(~200ms 节流)*/
int64_t pos = rate > 0 ? frames_done * 1000 / rate : 0;
pthread_mutex_lock(&g_play.mu);
@@ -763,6 +838,9 @@ static void run_song(int idx)
ncm_dec_close(dec);
/* 下载完成由播放线程 commit(rename) / 中断 abort(阶段 2 语义,§21) */
song_src_dispose(&ss);
song_gov_cleanup(&ss); /* page-cache 治理(A)曲终:注销 L1 条目 + commit 成功则 L0 整文件 DONTNEED */
mem_watch_song_end(); /* 调度器释放钩子:上一首缓冲完全清空(decoder 关 + 下载线程
* join + 源关 + 缓冲 free + .part 收尾)——给下一首让内存 */
/* 若应继续播下一首且非因命令打断,由外层处理(外层见 cur==idx 且需自然结束) */
}
@@ -835,7 +913,9 @@ static void *lyric_worker(void *arg)
}
if (!id[0]) continue;
if (strcmp(id, last_id) == 0) continue; /* 同一曲已拉/在拉 */
if (meow_cachegov_prefetch_paused()) continue; /* page-cache 治理(§2.3/§3):暂停歌词预取
* (A 回退硬暂停 OR svc_perf C Cached 降载),
* 不置 last_id → 降载恢复后下轮再拉。 */
ncm_strlcpy(last_id, id, sizeof last_id);
lyric_clear();
pthread_mutex_lock(&g_lyrmu);
@@ -898,6 +978,7 @@ int player_manager_start(void)
g_cache_gb = g_st.cache_limit_gb;
pthread_mutex_init(&g_play.mu, NULL);
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
if (ncm_thread_create(&g_thr, player_thread, NULL) != 0) return -1;
g_thr_on = 1;
return 0;
@@ -908,6 +989,7 @@ void player_manager_stop_thread(void)
g_want_run = 0;
g_app_quit = 1;
audio_out_close(); /* 打断阻塞写 */
mem_watch_stop(); /* 停监控线程 */
if (g_thr_on) {
pthread_join(g_thr, NULL);
g_thr_on = 0;
+36
View File
@@ -22,6 +22,7 @@
#define REBUFFER_AFTER_MS 500 /* 单次调用累计等待超此值后启用再缓冲迟滞 */
#define REBUFFER_MIN (64 * 1024) /* 迟滞放行门槛(≈1.6s@320kbps) */
#define STALL_MS 1000 /* 单次调用等待超此值 → 置 stalled(播放线程 pause 联动) */
#define STREAM_SRC_REFAULT_MS 50 /* pread 慢于此阈且数据已在盘上 → 计一次 refault(§2.3) */
struct stream_src {
int fd; /* 只读 fd(pread;写端另有自己的写 fd) */
@@ -36,8 +37,16 @@ struct stream_src {
uint64_t wait_ms_total;
int underruns;
int stalled;
int refaults; /* 数据在盘上但 pread 慢(>50ms)= DONTNEED 丢页重读 TF(§2.3) */
};
/* SDK meow_cachegov 提供的 refault 上报(strong 时覆盖弱符号;未链 SDK 时为空指针,调用需判空)。 */
#if defined(__GNUC__)
extern void meow_cachegov_note_refault(void) __attribute__((weak));
#else
extern void meow_cachegov_note_refault(void);
#endif
static int64_t mono_ms(void)
{
struct timespec ts;
@@ -182,9 +191,18 @@ static size_t src_read(void *buf, size_t size, void *ud)
if (avail > 0 && !gated) {
size_t n = (size - done) < (size_t)avail ? (size - done) : (size_t)avail;
uint64_t pos = s->read_pos;
int had_data = (pos < s->written); /* 数据已在盘上(放锁前采样;勿在放锁后读 written) */
pthread_mutex_unlock(&s->mu); /* pread 期间放锁,不阻塞写端 broadcast */
int64_t t0 = mono_ms();
ssize_t r = pread(s->fd, (char *)buf + done, n, (off_t)pos);
int64_t pread_ms = mono_ms() - t0;
pthread_mutex_lock(&s->mu);
if (r > 0 && had_data && pread_ms > STREAM_SRC_REFAULT_MS) {
/* 数据在盘上但 pread 慢 = DONTNEED 丢页后重读 TF(underrun 确认是 refault,
* 非网络等待)。上报 SDK meow_cachegov(弱符号,未链时为空指针需判空)。 */
s->refaults++;
if (meow_cachegov_note_refault) meow_cachegov_note_refault();
}
if (r <= 0) { /* 盘 IO 错:按中断收尾(真 EOF 语义) */
if (!s->failed) s->failed = -3; /* NCM_ERR_IO(本层不依赖 core 头) */
s->final = s->written ? s->written : 1;
@@ -290,6 +308,24 @@ uint64_t stream_src_written(stream_src_t *s)
return w;
}
int stream_src_refaults(stream_src_t *s)
{
if (!s) return 0;
pthread_mutex_lock(&s->mu);
int r = s->refaults;
pthread_mutex_unlock(&s->mu);
return r;
}
uint64_t stream_src_read_pos(stream_src_t *s)
{
if (!s) return 0;
pthread_mutex_lock(&s->mu);
uint64_t p = s->read_pos;
pthread_mutex_unlock(&s->mu);
return p;
}
uint64_t stream_src_final(stream_src_t *s)
{
if (!s) return 1;
+9
View File
@@ -72,6 +72,15 @@ void stream_src_stats(stream_src_t *s, int *underruns, uint64_t *wait_ms);
* 取走即清零。 */
int stream_src_take_stalled(stream_src_t *s);
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE §2.3:underrun/refault 检测)----
* refault = pread 处「数据已在盘上(written>read_pos)但读出耗时 >50ms」——DONTNEED 丢页后
* 重读 TF 的信号,区别于网络 underrun(avail==0 等待)。SDK meow_cachegov 据此做 A 回退。
* 播放线程读 refaults 落 g_play 取证;回调由 SDK(meow_cachegov_note_refault)提供,
* 未链 SDK 时弱符号为空(host sim 兼容)。 */
int stream_src_refaults(stream_src_t *s);
/* 当前解码逻辑读指针(stream_src_seek_cb 改写)。 */
uint64_t stream_src_read_pos(stream_src_t *s);
uint64_t stream_src_written(stream_src_t *s);
uint64_t stream_src_final(stream_src_t *s); /* 0=下载中;>0=最终大小(完成=全长,中断=截断点) */
int stream_src_failed(stream_src_t *s); /* 0=无错误;<0=下载/IO 错误码 */