feat(launcher): 媒体会话+Now Playing (agent-204) — MSG_META(0x72)/MSG_PLAYCTL(0x73)+meow_media.c(最近活跃源=active_seq净序,30s超时回收); 通知中心顶部媒体卡片(封面缓存+进度seek+控制键); 主界面中部/右缘左滑进 Now Playing 大页(封面+元数据+进度+控制+歌词占位), 左缘返回; 空态完备

launcher a42171a1→ddfe616e; 宿主 test_media ALL PASS + qemu 端到端(META注册→PLAYCTL下发闭环)
出入: 卡片以媒体会话为权威非通知; PLAYCTL 走会话fd(断连防御留后续)
This commit is contained in:
MeowOS committed 2026-08-31 06:51:04 +08:00
1 parent 8012948b26
commit 32543ac616
15 files changed
+1429 -34

No files matched your search

+2
View File
@@ -1301,6 +1301,8 @@ static int launch_app_chain(struct app *a, const char *chain)
* 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */ * 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */
settings_load(); settings_load();
meow_nav_exit(); /* M3:前台应用会话结束,清导航栈 */ meow_nav_exit(); /* M3:前台应用会话结束,清导航栈 */
/* 阶段D附录B:源 App 退出 → 清其媒体会话(如该 App 上报过 META),通知中心/播放页回空态 */
meow_media_clear(a->id);
logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status)); logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status));
return 0; return 0;
} }
+2 -2
View File
@@ -25,9 +25,9 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
"$CC" -I"../sdk" -Os -static \ "$CC" -I"../sdk" -Os -static \
main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \ main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \
notif_center.c app_launcher.c audio.c backlight.c net.c about.c \ notif_center.c app_launcher.c audio.c backlight.c net.c about.c \
subpage.c wifi_page.c bt_page.c \ subpage.c wifi_page.c bt_page.c now_playing.c \
font_theme.c keys.c util.c \ font_theme.c keys.c util.c \
meow_touch.c meow_settings.c meow_notif.c \ meow_touch.c meow_settings.c meow_notif.c meow_media.c \
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd "../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
echo "== meow_launcherd ==" echo "== meow_launcherd =="
ls -la meow_launcherd ls -la meow_launcherd
+35 -2
View File
@@ -825,6 +825,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
/* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */ /* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */
meow_ipc_process(); meow_ipc_process();
meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/
meow_media_tick(); /* 阶段D附录B:媒体源死亡(播放中无 META 超时)清槽回空态 */
/* sim hooks */ /* sim hooks */
if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) { if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) {
@@ -1104,7 +1105,15 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */ * 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */
if (play && !m_prev_play) { if (play && !m_prev_play) {
any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */ any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */
logmsg("play key: media-reserved (no menu confirm)"); /* 阶段D附录B:媒体键路由到最近活跃媒体会话(播放/暂停切换)。无源=打点。 */
const meow_media_session_t *ms = meow_media_current();
if (ms) {
uint32_t cmd = (ms->state == MEOW_MEDIA_PLAYING) ? 2u /* 暂停 */ : 1u /* 播放 */;
int rc = meow_media_playctl(cmd, 0);
logmsg("play key: media control -> cmd=%u rc=%d (%s)", cmd, rc, ms->app_id);
} else {
logmsg("play key: media-reserved (no media source)");
}
} }
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play;
@@ -1296,7 +1305,31 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
continue; /* 本圈不再按 tap/其余处理 */ continue; /* 本圈不再按 tap/其余处理 */
} }
/* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、 /* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、
* 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */ * 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。
* 阶段D附录B:主界面「右缘/中部向左横滑」→ 进入 Now Playing 大页。
* 手势分流(MEOW_APP_SDK §5.2):顶部下滑=通知中心(已在上方处理)、
* 左缘右滑=返回(左缘起点不至此),此处只接管「中部/右缘向左」横滑。 */
if (dx <= -MENU_SWIPE_THRESH && adx > ady &&
tx >= MEOW_EDGE_W && ty >= SB_BAR_H) {
logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy);
menu_arm_touch(TAP_ARM_DELAY_MS);
run_now_playing();
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
dirty = 1;
continue;
}
if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) { if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) {
logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy); logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy);
} else if (tx >= 0 && ty >= 0) { } else if (tx >= 0 && ty >= 0) {
Binary file not shown.
+144
View File
@@ -342,6 +342,126 @@ static int cmd_dismissall(void)
return 1; return 1;
} }
/* 阶段 D 附录 B:媒体会话上报 → MSG_META,期望 MSG_ACK。
* meta <app_id> <state> <pos_ms> <dur_ms> <title> <artist> <album> [artwork] */
static int cmd_meta(const char *app_id, uint32_t state, uint32_t pos, uint32_t dur,
const char *title, const char *artist, const char *album, const char *artwork)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_meta_t mm;
memset(&mm, 0, sizeof mm);
strncpy(mm.app_id, app_id, MEOW_APP_ID_MAX - 1);
mm.state = state; mm.pos_ms = pos; mm.dur_ms = dur;
snprintf(mm.title, sizeof mm.title, "%s", title);
snprintf(mm.artist, sizeof mm.artist, "%s", artist);
snprintf(mm.album, sizeof mm.album, "%s", album);
if (artwork) snprintf(mm.artwork, sizeof mm.artwork, "%s", artwork);
uint8_t rbuf[256];
int rt = xchg(fd, MSG_META, 11, &mm, sizeof mm, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("META ok: app=%s state=%u code=%d\n", app_id, state, a.code);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "META: unexpected resp type 0x%04x\n", rt);
return 1;
}
/* 控制转发 → MSG_PLAYCTL(launcher 转发到最近活跃媒体会话),期望 MSG_ACK。
* playctl <cmd> [seek_ms] (cmd 1=播放 2=暂停 3=上一首 4=下一首 5=seek) */
static int cmd_playctl(uint32_t cmd, int64_t seek_ms)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_playctl_t c;
memset(&c, 0, sizeof c);
c.cmd = cmd; c.seek_ms = seek_ms;
uint8_t rbuf[256];
int rt = xchg(fd, MSG_PLAYCTL, 12, &c, sizeof c, rbuf, sizeof rbuf);
close(fd);
if (rt == MSG_ACK) {
m_ack_t a;
memcpy(&a, rbuf, sizeof a);
printf("PLAYCTL ok: cmd=%u seek=%lld code=%d (status=%d)\n", cmd,
(long long)seek_ms, a.code, (int)a.status);
return a.code == MEOW_OK ? 0 : 1;
}
if (rt == MSG_ERR) { print_err((const m_err_t *)rbuf); return 1; }
fprintf(stderr, "PLAYCTL: unexpected resp type 0x%04x\n", rt);
return 1;
}
/* 读一帧(头+负载)→ 返回负载长;-1=连接断/坏帧。type/req_id 输出。 */
static int read_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *rbuf, uint32_t rcap)
{
uint8_t rhdr[MEOW_IPC_HDR_LEN];
size_t off = 0;
while (off < sizeof rhdr) {
ssize_t r = read(fd, rhdr + off, sizeof rhdr - off);
if (r <= 0) return -1;
off += (size_t)r;
}
uint32_t magic, len; uint16_t ver;
memcpy(&magic, rhdr, 4); memcpy(&ver, rhdr + 4, 2); memcpy(type, rhdr + 6, 2);
memcpy(req_id, rhdr + 8, 4); memcpy(&len, rhdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
if (len > rcap) len = rcap;
off = 0;
while (off < len) {
ssize_t r = read(fd, rbuf + off, len - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return (int)len;
}
/* 媒体源监听:HELLO(MF_PLAYBACK) + META → 保持连接等待 launcher 下发 MSG_PLAYCTL。
* listen <app_id> <state> <pos_ms> <dur_ms> <title> <artist> <album> [artwork] */
static int cmd_listen(const char *app_id, uint32_t state, uint32_t pos, uint32_t dur,
const char *title, const char *artist, const char *album,
const char *artwork)
{
int fd = connect_path(g_sock);
if (fd < 0) return 1;
m_hello_t h;
memset(&h, 0, sizeof h);
strncpy(h.app_id, app_id, MEOW_APP_ID_MAX - 1);
h.version = 1; h.flags = MF_PLAYBACK;
uint8_t rbuf[256];
int rt = xchg(fd, MSG_HELLO, 1, &h, sizeof h, rbuf, sizeof rbuf);
if (rt != MSG_ACK) { fprintf(stderr, "listen: HELLO resp 0x%04x\n", rt); close(fd); return 1; }
m_meta_t mm;
memset(&mm, 0, sizeof mm);
strncpy(mm.app_id, app_id, MEOW_APP_ID_MAX - 1);
mm.state = state; mm.pos_ms = pos; mm.dur_ms = dur;
snprintf(mm.title, sizeof mm.title, "%s", title);
snprintf(mm.artist, sizeof mm.artist, "%s", artist);
snprintf(mm.album, sizeof mm.album, "%s", album);
if (artwork) snprintf(mm.artwork, sizeof mm.artwork, "%s", artwork);
rt = xchg(fd, MSG_META, 2, &mm, sizeof mm, rbuf, sizeof rbuf);
if (rt != MSG_ACK) { fprintf(stderr, "listen: META resp 0x%04x\n", rt); close(fd); return 1; }
printf("listen: source=%s registered (state=%u) — awaiting PLAYCTL...\n", app_id, state);
fflush(stdout);
/* 阻塞读:等待 launcher 下发 MSG_PLAYCTL(同一连接)。 */
for (;;) {
uint16_t type; uint32_t req_id;
int n = read_frame(fd, &type, &req_id, rbuf, sizeof rbuf);
if (n < 0) { fprintf(stderr, "listen: conn closed\n"); close(fd); return 0; }
if (type == MSG_PLAYCTL && n >= (int)sizeof(m_playctl_t)) {
m_playctl_t c;
memcpy(&c, rbuf, sizeof c);
printf("RECV PLAYCTL: cmd=%u seek=%lld\n", c.cmd, (long long)c.seek_ms);
fflush(stdout);
} else {
printf("RECV frame type=0x%04x len=%d\n", type, n);
}
}
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
const char *env = getenv("MEOW_IPC_SOCK"); const char *env = getenv("MEOW_IPC_SOCK");
@@ -408,6 +528,30 @@ int main(int argc, char **argv)
if (!strcmp(cmd, "dismissall")) { if (!strcmp(cmd, "dismissall")) {
return cmd_dismissall(); return cmd_dismissall();
} }
if (!strcmp(cmd, "meta")) {
/* meta <app_id> <state> <pos_ms> <dur_ms> <title> <artist> <album> [artwork] */
if (i + 7 >= argc) { fprintf(stderr, "meta needs <app_id> <state> <pos_ms> <dur_ms> <title> <artist> <album>\n"); return 1; }
uint32_t st = (uint32_t)strtoul(argv[i + 2], NULL, 0);
uint32_t pos = (uint32_t)strtoul(argv[i + 3], NULL, 0);
uint32_t dur = (uint32_t)strtoul(argv[i + 4], NULL, 0);
const char *aw = (i + 8 < argc) ? argv[i + 8] : NULL;
return cmd_meta(argv[i + 1], st, pos, dur, argv[i + 5], argv[i + 6], argv[i + 7], aw);
}
if (!strcmp(cmd, "listen")) {
/* listen <app_id> <state> <pos_ms> <dur_ms> <title> <artist> <album> [artwork] */
if (i + 7 >= argc) { fprintf(stderr, "listen needs <app_id> <state> <pos_ms> <dur_ms> <title> <artist> <album>\n"); return 1; }
uint32_t st = (uint32_t)strtoul(argv[i + 2], NULL, 0);
uint32_t pos = (uint32_t)strtoul(argv[i + 3], NULL, 0);
uint32_t dur = (uint32_t)strtoul(argv[i + 4], NULL, 0);
const char *aw = (i + 8 < argc) ? argv[i + 8] : NULL;
return cmd_listen(argv[i + 1], st, pos, dur, argv[i + 5], argv[i + 6], argv[i + 7], aw);
}
if (!strcmp(cmd, "playctl")) {
if (i + 1 >= argc) { fprintf(stderr, "playctl needs <cmd> [seek_ms]\n"); return 1; }
uint32_t cmd2 = (uint32_t)strtoul(argv[i + 1], NULL, 0);
int64_t seek = (i + 2 < argc) ? (int64_t)strtoll(argv[i + 2], NULL, 0) : 0;
return cmd_playctl(cmd2, seek);
}
if (!strcmp(cmd, "nav")) { if (!strcmp(cmd, "nav")) {
/* nav report <app_id> <depth> <top> | nav exit <app_id> */ /* nav report <app_id> <depth> <top> | nav exit <app_id> */
if (i + 1 >= argc) { fprintf(stderr, "nav needs <app_id> and report|exit\n"); return 1; } if (i + 1 >= argc) { fprintf(stderr, "nav needs <app_id> and report|exit\n"); return 1; }
+40 -2
View File
@@ -13,8 +13,9 @@
* uint32_t len; = payload 字节数 * uint32_t len; = payload 字节数
* }; * };
* *
* 本阶段(D · M0+M1+M2+M3 前置)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT + * 本阶段(D · M0+M1+M2+M3+M4 前置)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT +
* MSG_ACT_FOCUS/BACK + MSG_NOTIFY/PROGRESS/QUERY/DISMISS* + MSG_NAV(导航栈上报/退出)。 * MSG_ACT_FOCUS/BACK + MSG_NOTIFY/PROGRESS/QUERY/DISMISS* + MSG_NAV(导航栈上报/退出)
* + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)。
* picker(0x0070/0x0071)仅预留协议位,本批不实现。 * picker(0x0070/0x0071)仅预留协议位,本批不实现。
*/ */
#ifndef MEOW_IPC_H #ifndef MEOW_IPC_H
@@ -67,6 +68,8 @@ enum {
* 待 M3 延伸阶段定义(m_picker_call_t / m_picker_result_t),此处仅固定消息号与方向。 */ * 待 M3 延伸阶段定义(m_picker_call_t / m_picker_result_t),此处仅固定消息号与方向。 */
MSG_PICKER_CALL = 0x0070, /* ↑ 请求跨 App picker 调用(发起方 A → 持有方 B) */ MSG_PICKER_CALL = 0x0070, /* ↑ 请求跨 App picker 调用(发起方 A → 持有方 B) */
MSG_PICKER_RESULT = 0x0071, /* ↓ picker 返回值(候选结果回 A) */ MSG_PICKER_RESULT = 0x0071, /* ↓ picker 返回值(候选结果回 A) */
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */ MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */
}; };
@@ -90,6 +93,11 @@ enum {
#define MF_BACKGROUND 0x00000001u /* 可后台常驻 */ #define MF_BACKGROUND 0x00000001u /* 可后台常驻 */
#define MF_PLAYBACK 0x00000002u /* 后台音频/播放 */ #define MF_PLAYBACK 0x00000002u /* 后台音频/播放 */
/* ---- MSG_META flags(元数据有效位)---- */
#define MF_ART 0x00000004u /* 封面/专辑图已提供 */
#define MF_LYRIC 0x00000008u /* 歌词已提供 */
#define MF_COVER 0x00000010u /* artwork 为可用路径/槽位 */
/* ---- 标识/长度 ---- */ /* ---- 标识/长度 ---- */
#define MEOW_APP_ID_MAX 32 #define MEOW_APP_ID_MAX 32
#define MEOW_ACT_MAX 64 #define MEOW_ACT_MAX 64
@@ -164,6 +172,36 @@ typedef struct m_nav {
char current[MEOW_ACT_MAX]; /* REPORT: 当前顶层 activity_id;EXIT: 可空 */ char current[MEOW_ACT_MAX]; /* REPORT: 当前顶层 activity_id;EXIT: 可空 */
} m_nav_t; } m_nav_t;
/* MSG_META ↑:媒体会话实时上报(阶段 D 附录 B;FRAMEWORK_DESIGN 附录 B 的 m_meta 微调落实)。
* 声明 MF_PLAYBACK 的 App 首次上报 → 注册为媒体源;后续更新(含 1s 级进度)。字段全 LE 定长。
* - state:0=停止 1=播放 2=暂停 3=缓冲。
* - flags:MF_ART/MF_LYRIC/MF_COVER(可多;声明哪类元数据有效)。
* - artwork/lyric:封面/歌词源(本地路径或 MeowOS 存储槽位);空=无(mf 位同时清)。
* - app_id:上报源头(与 HELLO 的 app_id 一致),launcher 据此归并同一会话。
* 边界:防越界(128 长度由 meow_ipc 双方同源 + 接收侧截断;payload < MEOW_IPC_CARGO)。 */
typedef struct m_meta {
char title[128];
char artist[128];
char album[128];
uint32_t pos_ms; /* 当前播放位置 */
uint32_t dur_ms; /* 总时长 */
uint32_t state; /* 0=停止 1=播放 2=暂停 3=缓冲 */
uint32_t flags; /* MF_ART / MF_LYRIC / MF_COVER */
char artwork[192]; /* 封面: 本地路径或 MeowOS 存储槽位 */
char lyric[192]; /* 歌词: 路径或文本槽位 */
char app_id[MEOW_APP_ID_MAX];
} m_meta_t; /* 128+128+128+16+192+192+32 = 828 B */
/* MSG_PLAYCTL ↓(launcher→App)/↑(控制器→launcher 转发到最近活跃媒体会话):播放控制。
* cmd:1=播放 2=暂停 3=上一首 4=下一首 5=seek;其余保留。
* seek_ms:cmd=5 时目标位置(ms);其余置 0。
* 语义:通知中心卡片/播放页按钮、物理媒体键 → meow_media_playctl(cmd,seek_ms) 经
* 「最近活跃媒体会话」的连接 fd 下发;宿主验证经 meow_ipc playctl 发到 launcher 转发。 */
typedef struct m_playctl {
uint32_t cmd; /* 1=播放 2=暂停 3=上一首 4=下一首 5=seek */
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。 /* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon` * - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退); * (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
@@ -36,6 +36,7 @@ extern "C" {
#include "meow_ipc.h" /* M0+M1:IPC 协议帧/消息/负载定(与 meow_ipc 工具同源) */ #include "meow_ipc.h" /* M0+M1:IPC 协议帧/消息/负载定(与 meow_ipc 工具同源) */
#include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */ #include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */ #include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
#include "meow_media.h" /* 阶段D附录B:媒体会话注册表(MSG_META/MSG_PLAYCTL/最近活跃源) */
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */ #include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */ #include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */ #include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
@@ -686,6 +687,8 @@ void restore_backlight(void);
uint16_t rgb565(uint32_t c); uint16_t rgb565(uint32_t c);
void run_notif_center(struct app *apps, int napps, notif_click_t *click, void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int live_open, int lox, int loy); int live_open, int lox, int loy);
/* 阶段D附录B:Now Playing 大页(主界面横划进入;左缘右滑返回;阻塞到返回)。 */
void run_now_playing(void);
void run_settings_page(void); void run_settings_page(void);
void s_about_open(void); void s_about_open(void);
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur); int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur);
Binary file not shown.
+202
View File
@@ -0,0 +1,202 @@
/* meow_media.c — MeowOS 媒体会话注册表(阶段 D · 附录 B / D1)。
*
* 实现说明见 meow_media.h。自包含:仅依赖 meow_ipc.h 结构 + <time.h>(monotonic)+
* <string.h>/<stdarg.h>;发送/日志经 meow_media_ops 注入,宿主可单测。
*/
#include "meow_media.h"
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
static const meow_media_ops *g_ops = NULL;
static meow_media_session_t g_sess[MEOW_MEDIA_MAX];
static uint64_t g_seq = 0; /* 全局活跃净序(每次 META +1,严格区分先后) */
/* 单调毫秒(self-contained;launcher/main_menu 的 now_ms 与此同源 CLOCK_MONOTONIC)。 */
static uint64_t media_now_ms(void)
{
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) ts.tv_sec = ts.tv_nsec = 0;
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000u);
}
static void media_log(const char *fmt, ...)
{
if (!g_ops || !g_ops->log) return;
char buf[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
g_ops->log("%s", buf);
}
static char *media_cp(char *dst, size_t dstsz, const char *src)
{
if (!dst || dstsz == 0) return dst;
if (!src) src = "";
snprintf(dst, dstsz, "%s", src);
dst[dstsz - 1] = 0;
return dst;
}
void meow_media_init(const meow_media_ops *ops)
{
g_ops = ops;
}
static meow_media_session_t *sess_find(const char *app_id)
{
if (!app_id || !app_id[0]) return NULL;
for (int i = 0; i < MEOW_MEDIA_MAX; i++)
if (g_sess[i].used && strcmp(g_sess[i].app_id, app_id) == 0)
return &g_sess[i];
return NULL;
}
/* 找空槽;满时覆盖最旧槽(LRU 式,防单源漂移占满)。 */
static meow_media_session_t *sess_alloc(void)
{
int slot = -1;
uint64_t oldest = UINT64_MAX;
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) return &g_sess[i];
if (g_sess[i].last_active_ms < oldest) { oldest = g_sess[i].last_active_ms; slot = i; }
}
if (slot >= 0) {
media_log("media: sessions full, evict app=%s", g_sess[slot].app_id);
memset(&g_sess[slot], 0, sizeof g_sess[slot]);
return &g_sess[slot];
}
return NULL;
}
int meow_media_meta(const m_meta_t *m, int fd)
{
if (!m || !m->app_id[0]) return -1;
meow_media_session_t *s = sess_find(m->app_id);
if (!s) {
s = sess_alloc();
if (!s) { media_log("media: meta drop app=%s (no slot)", m->app_id); return -1; }
s->used = 1;
media_cp(s->app_id, sizeof s->app_id, m->app_id);
}
s->fd = fd;
s->last_active_ms = media_now_ms();
s->active_seq = ++g_seq; /* 严格递增:跨会话先后唯一,不受时钟粒度影响 */
s->state = m->state;
s->pos_ms = m->pos_ms;
s->dur_ms = m->dur_ms;
s->flags = m->flags;
media_cp(s->title, sizeof s->title, m->title);
media_cp(s->artist, sizeof s->artist, m->artist);
media_cp(s->album, sizeof s->album, m->album);
media_cp(s->artwork, sizeof s->artwork, m->artwork);
media_cp(s->lyric, sizeof s->lyric, m->lyric);
media_log("media: META app=%s fd=%d state=%u pos=%u/%u title=\"%s\"",
s->app_id, fd, s->state, s->pos_ms, s->dur_ms, s->title);
return 0;
}
const meow_media_session_t *meow_media_current(void)
{
/* 最近活跃 = active_seq 最大者(严格先后;last_active_ms 仅作进度推进用)。 */
const meow_media_session_t *best = NULL;
uint64_t t = 0;
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (g_sess[i].used && g_sess[i].active_seq >= t) {
t = g_sess[i].active_seq;
best = &g_sess[i];
}
}
return best;
}
int meow_media_has_source(void)
{
const meow_media_session_t *s = meow_media_current();
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
}
static int send_playctl(const meow_media_session_t *s, uint32_t cmd, int64_t seek_ms)
{
if (!s || !s->used || !g_ops || !g_ops->send) return -1;
m_playctl_t c;
memset(&c, 0, sizeof c);
c.cmd = cmd;
c.seek_ms = seek_ms;
size_t n = g_ops->send(s->fd, MSG_PLAYCTL, 0, &c, sizeof c);
media_log("media: PLAYCTL -> %s cmd=%u seek=%lld n=%zu",
s->app_id, cmd, (long long)seek_ms, n);
/* ops->send 返回写入总字节(16B 头 + 负载);成功 = 完整帧已写出。 */
return (n >= MEOW_IPC_HDR_LEN + sizeof c) ? 0 : -1;
}
int meow_media_playctl(uint32_t cmd, int64_t seek_ms)
{
return send_playctl(meow_media_current(), cmd, seek_ms);
}
int meow_media_playctl_to(const char *app_id, uint32_t cmd, int64_t seek_ms)
{
meow_media_session_t *s = sess_find(app_id);
return s ? send_playctl(s, cmd, seek_ms) : -1;
}
void meow_media_clear(const char *app_id)
{
int n = 0;
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (g_sess[i].used &&
(!app_id || !app_id[0] || strcmp(g_sess[i].app_id, app_id) == 0)) {
media_log("media: clear app=%s", g_sess[i].app_id);
memset(&g_sess[i], 0, sizeof g_sess[i]);
n++;
}
}
if (app_id && app_id[0]) media_log("media: cleared %d sess for %s", n, app_id);
else if (n) media_log("media: cleared all %d sess", n);
}
void meow_media_tick(void)
{
uint64_t now = media_now_ms();
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) continue;
if (g_sess[i].state == MEOW_MEDIA_PLAYING || g_sess[i].state == MEOW_MEDIA_BUFFER) {
if (now - g_sess[i].last_active_ms > MEOW_MEDIA_STALE_MS) {
media_log("media: stale source %s (no META %llu ms) -> clear",
g_sess[i].app_id,
(unsigned long long)(now - g_sess[i].last_active_ms));
memset(&g_sess[i], 0, sizeof g_sess[i]);
}
}
}
}
uint32_t meow_media_display_pos(const meow_media_session_t *s)
{
if (!s) return 0;
uint32_t p = s->pos_ms;
if (s->state == MEOW_MEDIA_PLAYING) {
uint64_t now = media_now_ms();
uint64_t el = (now > s->last_active_ms) ? (now - s->last_active_ms) : 0;
uint32_t adv = (el > 0xFFFFFFFFull) ? 0xFFFFFFFFu : (uint32_t)el;
p = s->pos_ms + adv;
if (s->dur_ms && p > s->dur_ms) p = s->dur_ms;
}
return p;
}
void meow_media_dump(const char *prefix)
{
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) continue;
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\"",
prefix ? prefix : "media", i, g_sess[i].app_id, g_sess[i].state,
g_sess[i].fd, (unsigned long long)g_sess[i].last_active_ms,
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title);
}
}
+93
View File
@@ -0,0 +1,93 @@
/* meow_media.h — MeowOS 媒体会话(阶段 D · 附录 B / D1)。
*
* 定位:媒体会话注册表(App 声明 MF_PLAYBACK + 上报 MSG_META → 成为媒体源;
* 最近活跃者=当前源)。通知中心媒体卡片 / Now Playing 页 / 物理媒体键 / 控制器统一消费;
* PLAYCTL 经会话的连接 fd 直接下发。语义对齐 FRAMEWORK_DESIGN 附录 B 的「启动器表现」与
* 附录 D1(MPRIS 最近活跃源选择)。
*
* 设计:
* - 会话=一个 App 一个槽(app_id 归并;单 App 恒 1 槽,防多发 META 占满)。
* - 当前源=last_active_ms 最大者(最近上报 META)。多源时此即「最近活跃」。
* - 判定源死亡:播放中(1)/缓冲(3) 超 MEOW_MEDIA_STALE_MS 无 META → 清槽回空态(App 死/
* 服务断);暂停(2)/停止(0) 不超时(暂停属合法驻留,停止由 state 判空态)。
* - 空态:无会话,或当前源 state==0(停止)。
*
* 自包含:不依赖 launcher static 符号;发送/日志经 meow_media_ops 注入(launcher 用
* ipc_send / logmsg;宿主单测用 socket write / NULL)。可独立单测(test_media.c)。
*/
#ifndef MEOW_MEDIA_H
#define MEOW_MEDIA_H
#include <stdint.h>
#include <stddef.h>
#include "meow_ipc.h"
/* 同时维护的媒体会话上限(防御;一个 App 一槽) */
#define MEOW_MEDIA_MAX 4
/* 播放中(1)/缓冲(3) 无 META 超此毫秒 → 判源死清槽(回空态);暂停/停止不超时 */
#define MEOW_MEDIA_STALE_MS 30000u
/* state 取值(与 m_meta_t.state 一致) */
#define MEOW_MEDIA_STOPPED 0u
#define MEOW_MEDIA_PLAYING 1u
#define MEOW_MEDIA_PAUSED 2u
#define MEOW_MEDIA_BUFFER 3u
/* 媒体会话(一个 App 一个槽)。fd=最近上报 META 的连接 fd(下发 PLAYCTL 用)。 */
typedef struct meow_media_session {
int used;
int fd;
char app_id[MEOW_APP_ID_MAX];
uint64_t last_active_ms; /* 最近 META 时刻(monotonic ms,进度推进用) */
uint64_t active_seq; /* 最近活跃净序(全局递增,供「最近活跃」严格排序) */
uint32_t state;
uint32_t pos_ms;
uint32_t dur_ms;
uint32_t flags;
char title[128];
char artist[128];
char album[128];
char artwork[192];
char lyric[192];
} meow_media_session_t;
/* 依赖注入:send=发一帧(launcher 用 ipc_send 包装;宿主单测用 socket 写);
* log=日志(launcher 用 logmsg;NULL=静默)。send 返回写入字节。 */
typedef struct meow_media_ops {
size_t (*send)(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len);
void (*log)(const char *fmt, ...);
} meow_media_ops;
/* 注入 ops(send/log);NULL 清空。launcher 启动时(meow_ipc_init)调用一次。 */
void meow_media_init(const meow_media_ops *ops);
/* App 上报 META:按 m->app_id 归并到会话(无则建槽);记录 fd/last_active;
* 截断安全(不越界)。返回 0=成功,-1=会话满/参数非法。 */
int meow_media_meta(const m_meta_t *m, int fd);
/* 当前源 = 最近活跃会话(last_active_ms 最大)。无会话返回 NULL。 */
const meow_media_session_t *meow_media_current(void);
/* 是否有活动媒体源:当前源存在且 state != 0(停止)。通知中心卡片/播放页据此显示。 */
int meow_media_has_source(void);
/* 向当前源下发 PLAYCTL(卡片按钮/播放页/媒体键/控制器)。返回 0=已发,-1=无源/发送失败。 */
int meow_media_playctl(uint32_t cmd, int64_t seek_ms);
/* 向指定 app 的会话下发 PLAYCTL;无匹配会话返回 -1。 */
int meow_media_playctl_to(const char *app_id, uint32_t cmd, int64_t seek_ms);
/* 清某 app 的会话(App 退出/源死亡/主动停止时调)。app_id 空/NULL=清全部。 */
void meow_media_clear(const char *app_id);
/* 主循环周期调(~1s):播放中(1)/缓冲(3) 超时无 META → 清槽。 */
void meow_media_tick(void);
/* 显示用虚拟位置:播放中按墙钟推进 pos_ms(clamp 到 dur);暂停/缓冲/停止原样。 */
uint32_t meow_media_display_pos(const meow_media_session_t *s);
/* 调试打印(经 ops->log,前缀+每会话一行)。 */
void meow_media_dump(const char *prefix);
#endif /* MEOW_MEDIA_H */
+212 -28
View File
@@ -26,6 +26,34 @@ static int g_ncp_swipe_row = -1;
static int g_ncp_swipe_off = 0; static int g_ncp_swipe_off = 0;
/* ---- 阶段D附录B:通知中心顶部媒体卡片(固定于通知列表上方,活动媒体会话时显示)---- */
#define NCP_MEDIA_H 112 /* 卡片高(封面/信息/控制/进度) */
#define NCP_MEDIA_CARD_X 12
#define NCP_MEDIA_CARD_W (fb_xres - 24)
static meow_img *g_ncp_media_cover = NULL; /* 封面缓存(按 artwork 路径变化重载,不每帧解码) */
static char g_ncp_media_cover_path[200] = "";
/* 卡片顶缘(恒定 = 通知列表区上缘)。 */
static int ncp_media_y(void) { return NCP_LIST_TOP; }
/* 卡片是否显示(活动媒体会话存在且非停止)。 */
static int ncp_media_on(void) { return meow_media_has_source() ? NCP_MEDIA_H : 0; }
/* 通知列表可视区实际顶缘(含媒体卡片时下移一个卡片高)——绘制/命中统一用,避免硬编码 NCP_LIST_TOP。 */
static int ncp_list_top(void) { return NCP_LIST_TOP + ncp_media_on(); }
/* 列表可视区高(含媒体卡片时缩短)。 */
static int ncp_list_view(void) { int v = NCP_LIST_BOT - ncp_list_top(); return v > 0 ? v : 0; }
/* 卡片内按钮几何(供绘制与命中共用)。 */
#define NCP_MEDIA_BTN_Y 34 /* 相对卡片顶的按钮中心 y */
#define NCP_MEDIA_PREV_CX 300
#define NCP_MEDIA_PLAY_CX 372
#define NCP_MEDIA_NEXT_CX 444
#define NCP_MEDIA_BTN_R 22 /* play 半径;prev/next 略小 */
#define NCP_MEDIA_PROG_Y 74 /* 相对卡片顶的进度条 top */
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */ /* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
static void ncp_strncpy(char *dst, const char *src, size_t len) static void ncp_strncpy(char *dst, const char *src, size_t len)
{ {
@@ -432,25 +460,25 @@ static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th)
static void ncp_draw_list(int scroll) static void ncp_draw_list(int scroll)
{ {
const meow_ui_theme_t *th = s_ui_theme(); const meow_ui_theme_t *th = s_ui_theme();
int lt = ncp_list_top(), lv = ncp_list_view();
if (g_nrows == 0) { if (g_nrows == 0) {
ui_draw(NCP_ROW_X + 8, NCP_LIST_TOP + 40, "暂无通知", th->on_surface_variant, 22); ui_draw(NCP_ROW_X + 8, lt + 40, "暂无通知", th->on_surface_variant, 22);
return; return;
} }
int total = g_nrows * NCP_ROW_H; int total = g_nrows * NCP_ROW_H;
scroll = mnp_scroll_clamp(scroll, total, NCP_LIST_BOT - NCP_LIST_TOP); scroll = mnp_scroll_clamp(scroll, total, lv);
for (int i = 0; i < g_nrows; i++) { for (int i = 0; i < g_nrows; i++) {
int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll; int ry = lt + i * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */ if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */
int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */ int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */
if (off < 0) ncp_draw_delete_bg(ry, off, th); if (off < 0) ncp_draw_delete_bg(ry, off, th);
ncp_draw_row(i, ry, off, th); ncp_draw_row(i, ry, off, th);
} }
/* 滚动条 */ /* 滚动条 */
if (total > NCP_LIST_BOT - NCP_LIST_TOP) { if (total > lv) {
int view = NCP_LIST_BOT - NCP_LIST_TOP; int thumb_h = lv * lv / total; if (thumb_h < 24) thumb_h = 24;
int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24; int ty = lt + (lv - thumb_h) * scroll / (total - lv);
int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view); meow_gfx_fill_round(fb_xres - 8, lt, 5, lv, 3, th->surface_container);
meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 3, th->surface_container);
meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant); meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant);
} }
} }
@@ -462,9 +490,10 @@ static void ncp_draw_list(int scroll)
static void ncp_repaint_list(int scroll) static void ncp_repaint_list(int scroll)
{ {
const meow_ui_theme_t *th = s_ui_theme(); const meow_ui_theme_t *th = s_ui_theme();
fill_rect(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP, th->surface_variant); int lt = ncp_list_top(), lv = ncp_list_view();
fill_rect(0, lt, fb_xres, lv, th->surface_variant);
ncp_draw_list(scroll); ncp_draw_list(scroll);
meow_dirty_add(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP); meow_dirty_add(0, lt, fb_xres, lv);
meow_ui_flush(); meow_ui_flush();
} }
@@ -475,8 +504,9 @@ static void ncp_repaint_row(int scroll, int row, int off)
{ {
const meow_ui_theme_t *th = s_ui_theme(); const meow_ui_theme_t *th = s_ui_theme();
if (row < 0 || row >= g_nrows) return; if (row < 0 || row >= g_nrows) return;
int ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll; int lt = ncp_list_top();
if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */ int ry = lt + row * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */
/* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */ /* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */
fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant); fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant);
if (off < 0) ncp_draw_delete_bg(ry, off, th); if (off < 0) ncp_draw_delete_bg(ry, off, th);
@@ -577,6 +607,139 @@ static int ncp_clear_all(void)
/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。 /* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。
* 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过, * 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过,
* 使脏区仅限面板体,绝不整屏重绘。 */ * 使脏区仅限面板体,绝不整屏重绘。 */
/* ================= 阶段D附录B:顶部媒体卡片(绘制/命中) ================= */
/* 封面缓存:artwork 路径变化才重载(不每帧解码)。 */
static void ncp_media_cover_load(const meow_media_session_t *s)
{
if (!s) return;
if (strcmp(g_ncp_media_cover_path, s->artwork) == 0) return;
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
ncp_strncpy(g_ncp_media_cover_path, s->artwork, sizeof g_ncp_media_cover_path);
if (!s->artwork[0]) return;
char path[200];
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
g_ncp_media_cover = meow_img_load_file(path);
}
/* 实心三角(dir=+1 右向 / -1 左向);size=最大宽度 px。按扫描线 fill_rect 近似 */
static void ncp_media_tri(int cx, int cy, int size, int dir, uint32_t col)
{
int half = size / 2;
for (int dy = -half; dy <= half; dy++) {
int w = half + 1 - (dy < 0 ? -dy : dy); /* 顶宽 1 → 底宽 half+1 */
if (w <= 0) continue;
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
fill_rect(x0, cy + dy, w, 1, col);
}
}
/* 圆形按钮底 + 字形(kind:0=prev 1=play 2=next 3=pause 4=暂停态play)。 */
static void ncp_media_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
uint32_t fg = 0xFFFFFF;
if (kind == 0) { /* prev:左三角 + 左竖条 */
ncp_media_tri(cx + 4, cy, r - 4, -1, fg);
fill_rect(cx - 3, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 1) { /* play:右三角 */
ncp_media_tri(cx + 2, cy, r - 2, 1, fg);
} else if (kind == 2) { /* next:右三角 + 右竖条 */
ncp_media_tri(cx - 4, cy, r - 4, 1, fg);
fill_rect(cx + 1, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 3) { /* pause:双竖条 */
int bw = (r - 6) / 2; if (bw < 2) bw = 2;
fill_rect(cx - 7, cy - (r - 5), bw, (r - 5) * 2, fg);
fill_rect(cx + 7 - bw, cy - (r - 5), bw, (r - 5) * 2, fg);
}
}
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
static void ncp_draw_media_card(const meow_ui_theme_t *th)
{
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
int cy = ncp_media_y();
meow_gfx_fill_round(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H, 14, th->surface_container);
ncp_media_cover_load(s);
int cover_s = 88, cover_x = NCP_MEDIA_CARD_X + 12, cover_y = cy + 12;
if (g_ncp_media_cover) {
meow_gfx_fill_round(cover_x, cover_y, cover_s, cover_s, 12, th->surface_variant);
meow_ui_img_cover(cover_x + 4, cover_y + 4, cover_s - 8, g_ncp_media_cover);
} else {
meow_gfx_fill_round(cover_x, cover_y, cover_s, cover_s, 12, th->primary);
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 20);
ui_draw(cover_x + (cover_s - cw) / 2, cover_y + (cover_s - meow_font_line_height(20)) / 2,
t, 0xFFFFFF, 20);
}
int tx = cover_x + cover_s + 14;
int tw = (NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R - 8) - tx; /* 文本宽限到上一首按钮左缘前 8px */
if (tw < 0) tw = 0;
ncp_text(tx, cy + 14, s->title, 18, th->on_surface, tw);
ncp_text(tx, cy + 40, s->artist[0] ? s->artist : s->album, 16, th->on_surface_variant, tw);
/* 控制按钮 */
int by = cy + NCP_MEDIA_BTN_Y;
ncp_media_btn(NCP_MEDIA_PREV_CX, by, NCP_MEDIA_BTN_R - 4, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 /* pause */ : 1 /* play */;
ncp_media_btn(NCP_MEDIA_PLAY_CX, by, NCP_MEDIA_BTN_R, pk, th);
ncp_media_btn(NCP_MEDIA_NEXT_CX, by, NCP_MEDIA_BTN_R - 4, 2, th);
/* 进度条 + 时间 */
uint32_t pos = meow_media_display_pos(s);
int px = tx, pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px, py = cy + NCP_MEDIA_PROG_Y;
if (pw > 0) {
fill_rect(px, py, pw, 4, th->outline);
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * pw / s->dur_ms) : 0;
if (fw > pw) fw = pw;
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
}
}
/* 卡片命中:按钮/进度(含 seek)。返回 1=已处理(内部已下发 PLAYCTL);0=未命中。 */
static int ncp_media_card_tap(int tx, int ty)
{
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return 0;
int cy = ncp_media_y();
if (ty < cy || ty >= cy + NCP_MEDIA_H) return 0;
int by = cy + NCP_MEDIA_BTN_Y;
int cmd = 0; int64_t seek = 0;
if (tx >= NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_PREV_CX + NCP_MEDIA_BTN_R &&
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R) cmd = 3; /* 上一首 */
else if (tx >= NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_PLAY_CX + NCP_MEDIA_BTN_R &&
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R)
cmd = (s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u; /* 播放/暂停 */
else if (tx >= NCP_MEDIA_NEXT_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_NEXT_CX + NCP_MEDIA_BTN_R &&
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R) cmd = 4; /* 下一首 */
else if (s->dur_ms > 0) { /* 进度条 seek */
int px = NCP_MEDIA_CARD_X + 12 + 88 + 14;
int pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px;
int py = cy + NCP_MEDIA_PROG_Y;
if (pw > 0 && ty >= py - 12 && ty <= py + 16 && tx >= px && tx <= px + pw) {
int frac = (tx - px) * 1000 / pw;
seek = (int64_t)s->dur_ms * frac / 1000;
cmd = 5;
}
}
if (!cmd) return 0;
int rc = meow_media_playctl(cmd, seek);
logmsg("notif-card: media tap cmd=%u seek=%lld rc=%d", cmd, (long long)seek, rc);
return 1;
}
/* 仅重绘媒体卡片带(进度/封面/按钮;不整面板、不刷状态栏)——局部刷新用。 */
static void ncp_repaint_media_card(void)
{
const meow_ui_theme_t *th = s_ui_theme();
if (!meow_media_has_source()) return;
int cy = ncp_media_y();
fill_rect(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H, th->surface_variant);
ncp_draw_media_card(th);
meow_dirty_add(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H);
meow_ui_flush();
}
static void draw_notif_center(int scroll, int redraw_sb) static void draw_notif_center(int scroll, int redraw_sb)
{ {
const meow_ui_theme_t *th = s_ui_theme(); const meow_ui_theme_t *th = s_ui_theme();
@@ -657,6 +820,9 @@ static void draw_notif_center(int scroll, int redraw_sb)
/* ⑧ 一键清除按钮(勿扰对称处的右半段) */ /* ⑧ 一键清除按钮(勿扰对称处的右半段) */
ncp_draw_clear(th); ncp_draw_clear(th);
/* 阶段D附录B:顶部媒体卡片(活动媒体会话时显示,固定于列表上方;列表可视区随之下移) */
ncp_draw_media_card(th);
/* 通知列表 */ /* 通知列表 */
ncp_draw_list(scroll); ncp_draw_list(scroll);
@@ -722,7 +888,7 @@ static int ncp_slider_apply(int which, int tx, int save_now)
static int ncp_row_tap(int tx, int ty, int scroll, notif_click_t *click) static int ncp_row_tap(int tx, int ty, int scroll, notif_click_t *click)
{ {
(void)tx; /* 行命中仅依赖 ty/scroll;tx 保留作接口一致(0-warning) */ (void)tx; /* 行命中仅依赖 ty/scroll;tx 保留作接口一致(0-warning) */
int eff = ty - NCP_LIST_TOP + scroll; int eff = ty - ncp_list_top() + scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
if (row < 0 || row >= g_nrows) return 0; if (row < 0 || row >= g_nrows) return 0;
const ncp_row_t *r = &g_rows[row]; const ncp_row_t *r = &g_rows[row];
@@ -849,7 +1015,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0 /* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */ * 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
ncp_refresh_rows(); ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP); list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, ncp_list_view());
ncp_snap_init(); ncp_snap_init();
if (g_ncp_ph > 0) { if (g_ncp_ph > 0) {
anim_ok = 1; anim_ok = 1;
@@ -869,6 +1035,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
anim_last = now_ms(); anim_last = now_ms();
int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */ int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */
int media_prev = -1; /* 阶段D附录B:上次媒体卡片显示态(检测 出现/消失 → 整面板重排) */
int media_paint = 0; /* 阶段D附录B:仅重绘媒体卡片带(进度/封面变化,局部刷新) */
for (;;) { for (;;) {
unsigned long long now = now_ms(); unsigned long long now = now_ms();
@@ -1030,10 +1198,10 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int ddx = tlx - tdx, ddy = tly - tdy; int ddx = tlx - tdx, ddy = tly - tdy;
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy; int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) { if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
int eff = tdy - NCP_LIST_TOP + list_scroll; int eff = tdy - ncp_list_top() + list_scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */ /* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
if (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && if (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
row >= 0 && row < g_nrows) { row >= 0 && row < g_nrows) {
row_swipe_row = row; row_swipe_row = row;
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W); row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
@@ -1057,8 +1225,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy); int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
if (ady2 >= S_SCROLL_SLOP) { if (ady2 >= S_SCROLL_SLOP) {
int av = g_nrows * NCP_ROW_H; int av = g_nrows * NCP_ROW_H;
int vw = NCP_LIST_BOT - NCP_LIST_TOP; int vw = ncp_list_view();
int in_over = (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && int in_over = (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
mnp_scroll_max(av, vw) > 0); mnp_scroll_max(av, vw) > 0);
if (!in_over) { if (!in_over) {
int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph); int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph);
@@ -1081,9 +1249,9 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int ddy = tly - scroll_y0; int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
int total = g_nrows * NCP_ROW_H; int total = g_nrows * NCP_ROW_H;
int view = NCP_LIST_BOT - NCP_LIST_TOP; int view = ncp_list_view();
if (mnp_scroll_max(total, view) > 0 && if (mnp_scroll_max(total, view) > 0 &&
tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT) { tdy >= ncp_list_top() && tdy < NCP_LIST_BOT) {
scroll_drag = 1; scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert); meow_settings_get_bool("input.swipe_invert", &scroll_invert);
scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert); scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert);
@@ -1093,7 +1261,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
} }
if (scroll_drag && td_ok) { if (scroll_drag && td_ok) {
int total = g_nrows * NCP_ROW_H; int total = g_nrows * NCP_ROW_H;
int view = NCP_LIST_BOT - NCP_LIST_TOP; int view = ncp_list_view();
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert); int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
list_scroll = mnp_scroll_clamp(target, total, view); list_scroll = mnp_scroll_clamp(target, total, view);
scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view), scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view),
@@ -1147,7 +1315,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
* 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过 * 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过
* 下方 mnp_vertical_up 关闭判定。 */ * 下方 mnp_vertical_up 关闭判定。 */
int total = g_nrows * NCP_ROW_H; int total = g_nrows * NCP_ROW_H;
int view = NCP_LIST_BOT - NCP_LIST_TOP; int view = ncp_list_view();
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert); int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
list_scroll = mnp_scroll_clamp(target, total, view); list_scroll = mnp_scroll_clamp(target, total, view);
if (scroll_rebound) { if (scroll_rebound) {
@@ -1205,7 +1373,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
g_ncp_dnd = dv ? 0 : 1; g_ncp_dnd = dv ? 0 : 1;
ncp_refresh_rows(); /* 按 dnd 过滤重采集 */ ncp_refresh_rows(); /* 按 dnd 过滤重采集 */
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP); ncp_list_view());
hit = 1; need_paint = 1; hit = 1; need_paint = 1;
} }
/* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */ /* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */
@@ -1218,10 +1386,14 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows(); ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP); ncp_list_view());
hit = 1; need_paint = 1; hit = 1; need_paint = 1;
} }
if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) { /* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL。
* 卡片命中固定于列表上方,先于列表行命中处理。 */
if (!hit && meow_media_has_source() && ncp_media_card_tap(tx, ty)) {
hit = 1; need_paint = 1;
} else if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
if (ncp_row_tap(tx, ty, list_scroll, click)) { if (ncp_row_tap(tx, ty, list_scroll, click)) {
hit = 1; hit = 1;
if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */ if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */
@@ -1253,7 +1425,18 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
/* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */ /* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */
if (now - last_listchk >= 500) { if (now - last_listchk >= 500) {
last_listchk = now; last_listchk = now;
/* 阶段D附录B:面板驻留时排空 IPC(App 上报 META 更新媒体会话)+ 媒体会话 TTL,再做展示刷新 */
meow_ipc_process();
meow_notif_tick(); meow_notif_tick();
meow_media_tick();
int m_on = meow_media_has_source() ? 1 : 0;
if (m_on != media_prev) {
media_prev = m_on; /* 卡片出现/消失 → 列表顶缘变化,需整面板重排 */
need_paint = 1;
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, ncp_list_view());
} else if (m_on) {
media_paint = 1; /* 已有源:进度/封面随 META 变化 → 仅重绘卡片带 */
}
if (ncp_rows_changed()) { if (ncp_rows_changed()) {
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */ /* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
row_swipe_row = -1; row_off = 0; row_settle = 0; row_swipe_row = -1; row_off = 0; row_settle = 0;
@@ -1262,10 +1445,11 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0; panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
ncp_refresh_rows(); ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP); ncp_list_view());
need_paint = 1; need_paint = 1;
} }
} }
if (media_paint) { ncp_repaint_media_card(); media_paint = 0; }
/* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。 /* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。
* 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */ * 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */
@@ -1321,7 +1505,7 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click,
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows(); ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP); ncp_list_view());
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */ need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
} }
} }
+326
View File
@@ -0,0 +1,326 @@
/* now_playing.c — 阶段D附录B:Now Playing 大页(主界面横划进入;左缘右滑返回)。
*
* 定案(用户):Now Playing = 通知中心媒体卡片 + 主界面横划进入的播放页,两者都做。
* 本文件实现后者:封面大图 + 元数据 + 进度 + 控制按钮 + 歌词占位;返回手势回主界面。
* 数据源:meow_media 会话(最近活跃媒体源);空态=占位。参照 SDK §5.2 横向分页模板的
* 手势分流(左缘返回 / 顶部下滑通知中心 / 中部横滑内容)——本页只接管「中部/右缘向左滑进、
* 左缘右滑返回」,与全局手势不冲突。
*
* 性能:封面解码缓存(按 artwork 路径变化重载,不每帧解码);整页 1s 刷新进度(墙钟推进),
* 元数据/源变化立即整页重画;交互只重绘按钮/进度带。
*/
#include "meow_launcher_internal.h"
#define NP_COVER 300 /* 封面边长(方,居中) */
#define NP_COVER_X ((fb_xres - NP_COVER) / 2)
#define NP_COVER_Y 60
#define NP_TITLE_Y 412
#define NP_ARTIST_Y 450
#define NP_ALBUM_Y 480
#define NP_PROG_X 40
#define NP_PROG_W (fb_xres - 80)
#define NP_PROG_Y 518
#define NP_TIME_Y 492
#define NP_BTN_Y 568 /* 控制按钮中心 y */
#define NP_PLAY_CX (fb_xres / 2)
#define NP_PREV_CX (NP_PLAY_CX - 78)
#define NP_NEXT_CX (NP_PLAY_CX + 78)
#define NP_PLAY_R 30
#define NP_PREV_R 22
#define NP_LYRIC_Y 640
static meow_img *g_np_cover = NULL;
static char g_np_cover_path[200] = "";
/* 封面解码缓存(artwork 路径变化才重载)。 */
static void np_cover_load(const meow_media_session_t *s)
{
if (!s) return;
if (strcmp(g_np_cover_path, s->artwork) == 0) return;
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
snprintf(g_np_cover_path, sizeof g_np_cover_path, "%s", s->artwork);
if (!s->artwork[0]) return;
char path[200];
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
g_np_cover = meow_img_load_file(path);
}
/* 实心三角(dir=+1 右向 / -1 左向)。 */
static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
{
int half = size / 2;
for (int dy = -half; dy <= half; dy++) {
int w = half + 1 - (dy < 0 ? -dy : dy);
if (w <= 0) continue;
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
fill_rect(x0, cy + dy, w, 1, col);
}
}
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。 */
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
int W = fb_xres;
if (ui_width(s, size) <= maxw) {
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
return;
}
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
}
/* 圆形控制按钮(kind:0=prev 1=play 2=next 3=pause)。 */
static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
uint32_t fg = 0xFFFFFF;
if (kind == 0) {
np_tri(cx + 5, cy, r - 5, -1, fg);
fill_rect(cx - 4, cy - (r - 7), 4, (r - 7) * 2, fg);
} else if (kind == 1) {
np_tri(cx + 3, cy, r - 3, 1, fg);
} else if (kind == 2) {
np_tri(cx - 5, cy, r - 5, 1, fg);
fill_rect(cx + 2, cy - (r - 7), 4, (r - 7) * 2, fg);
} else {
int bw = (r - 8) / 2; if (bw < 2) bw = 2;
fill_rect(cx - 8, cy - (r - 7), bw, (r - 7) * 2, fg);
fill_rect(cx + 8 - bw, cy - (r - 7), bw, (r - 7) * 2, fg);
}
}
/* 进度带(进度条 + 时间)绘制;pts = 当前显示位置。 */
static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const meow_ui_theme_t *th)
{
fill_rect(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, th->outline);
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * NP_PROG_W / s->dur_ms) : 0;
if (fw > NP_PROG_W) fw = NP_PROG_W;
if (fw > 0) fill_rect(NP_PROG_X, NP_PROG_Y, fw, 6, th->primary);
/* 时间(左=当前,右=总长;MM:SS) */
char l[16], rbuf[16];
unsigned lm = pos / 60000u, ls = (pos % 60000u) / 1000u;
unsigned dm = (s->dur_ms ? s->dur_ms : 0) / 60000u, ds = (s->dur_ms ? s->dur_ms : 0) % 60000u / 1000u;
snprintf(l, sizeof l, "%u:%02u", lm, ls);
snprintf(rbuf, sizeof rbuf, "%u:%02u", dm, ds);
ui_draw(NP_PROG_X, NP_TIME_Y, l, th->on_surface_variant, 16);
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, th->on_surface_variant, 16);
}
/* 整页绘制(空态占位)。redraw_sb=1 时喂/标脏状态栏(打开/主题变化),否则保留上次顶条。 */
static void np_draw_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) {
/* 空态占位 */
const char *t1 = "未在播放";
ui_draw((W - ui_width(t1, 26)) / 2, 300, t1, th->on_surface_variant, 26);
const char *t2 = "打开播放 App 以显示正在播放";
ui_draw((W - ui_width(t2, 18)) / 2, 348, t2, th->on_surface_variant, 18);
const char *t3 = "左缘右滑返回";
ui_draw((W - ui_width(t3, 16)) / 2, 396, t3, th->on_surface_variant, 16);
if (redraw_sb) draw_statusbar();
return;
}
np_cover_load(s);
/* 封面:解码缓存 + 圆角裁方;无封面回退主题色占位 + 首字 */
if (g_np_cover) {
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
meow_ui_img_cover(NP_COVER_X, NP_COVER_Y, NP_COVER, g_np_cover);
} else {
meow_gfx_fill_round(NP_COVER_X - 6, NP_COVER_Y - 6, NP_COVER + 12, NP_COVER + 12, 18, th->surface_container);
meow_gfx_fill_round(NP_COVER_X, NP_COVER_Y, NP_COVER, NP_COVER, 14, th->primary);
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 22);
ui_draw(NP_COVER_X + (NP_COVER - cw) / 2, NP_COVER_Y + (NP_COVER - meow_font_line_height(22)) / 2,
t, 0xFFFFFF, 22);
}
/* 元数据 */
np_center_text(NP_TITLE_Y, s->title, 26, th->on_surface, W - 40);
np_center_text(NP_ARTIST_Y, s->artist, 18, th->on_surface_variant, W - 60);
if (s->album[0])
np_center_text(NP_ALBUM_Y, s->album, 16, th->on_surface_variant, W - 80);
/* 进度 */
uint32_t pos = meow_media_display_pos(s);
np_draw_progress(s, pos, th);
/* 控制按钮 */
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1;
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, pk, th);
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
/* 歌词占位 */
const char *ly = "歌词(占位)";
ui_draw((W - ui_width(ly, 16)) / 2, NP_LYRIC_Y, ly, th->on_surface_variant, 16);
if (redraw_sb) draw_statusbar();
}
/* 仅重绘进度带 + 按钮(元数据/源变化时整页;纯进度变化时局部)。 */
static void np_repaint_progress(void)
{
const meow_ui_theme_t *th = s_ui_theme();
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
uint32_t pos = meow_media_display_pos(s);
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏)。 */
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
np_draw_progress(s, pos, th);
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1, th);
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
meow_dirty_add(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y);
meow_ui_flush();
}
/* 控制按钮命中:返回 cmd(1-4),0=未命中。 */
static int np_btn_tap(int tx, int ty)
{
if (tx >= NP_PREV_CX - NP_PREV_R && tx <= NP_PREV_CX + NP_PREV_R &&
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 3;
if (tx >= NP_PLAY_CX - NP_PLAY_R && tx <= NP_PLAY_CX + NP_PLAY_R &&
ty >= NP_BTN_Y - NP_PLAY_R && ty <= NP_BTN_Y + NP_PLAY_R) {
const meow_media_session_t *s = meow_media_current();
return (s && s->state == MEOW_MEDIA_PLAYING) ? 2 : 1;
}
if (tx >= NP_NEXT_CX - NP_PREV_R && tx <= NP_NEXT_CX + NP_PREV_R &&
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 4;
return 0;
}
/* 进度 seek 命中(拖动/点按进度带):返回 seek_ms(>0),0=未中(或 dur=0)。 */
static int64_t np_progress_seek(int tx, int ty)
{
const meow_media_session_t *s = meow_media_current();
if (!s || !s->dur_ms) return 0;
if (tx >= NP_PROG_X && tx <= NP_PROG_X + NP_PROG_W &&
ty >= NP_PROG_Y - 14 && ty <= NP_PROG_Y + 20) {
int frac = (tx - NP_PROG_X) * 1000 / NP_PROG_W;
return (int64_t)s->dur_ms * frac / 1000;
}
return 0;
}
void run_now_playing(void)
{
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
struct keypoll kp_keys;
kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_play;
memset(&kd_play, 0, sizeof kd_play);
/* 读播放键按住态(用于边沿) */
int m_prev_play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
unsigned long long last_prog = now_ms();
int need_redraw = 1;
char last_title[128] = "";
uint32_t last_state = 0xFFFFFFFFu;
char last_app[MEOW_APP_ID_MAX] = "";
np_draw_page(1);
meow_dirty_full();
meow_ui_flush();
while (!sigterm_flag) {
meow_ipc_process();
meow_media_tick();
unsigned long long now = now_ms();
/* 触摸 */
if (tfd >= 0 && meow_screen_on) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8);
uint16_t code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) { tdx = tlx; tdy = tly; td_ok = 1; }
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() >= touch_cooldown_until) { td = 1; td_ok = 0; tlx = tly = -1; }
} else if (!val && td) {
td = 0;
if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
/* 左缘右滑返回(全局手势语义):回到主界面(不清理媒体会话) */
if (meow_edge_back(tdx, dx, dy)) break;
if (adx < 60 && ady < 60) {
int cmd = np_btn_tap(tlx, tly);
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
else {
int64_t seek = np_progress_seek(tlx, tly);
if (seek > 0) meow_media_playctl(5, seek);
}
}
tdx = tdy = -1;
} else { tdx = tdy = -1; }
}
}
}
}
}
/* 播放键边沿 → 播放/暂停切换 */
kp_poll(&kp_keys, NULL, NULL);
int play = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (play && !m_prev_play) {
const meow_media_session_t *s = meow_media_current();
if (s) meow_media_playctl((s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u, 0);
}
m_prev_play = play;
/* 媒体变化检测(元数据/源/状态):立即整页重画 */
const meow_media_session_t *s = meow_media_current();
int meta_changed = !s || strcmp(last_app, s->app_id) ||
strcmp(last_title, s->title) || last_state != s->state;
if (meta_changed) {
if (s) {
snprintf(last_app, sizeof last_app, "%s", s->app_id);
snprintf(last_title, sizeof last_title, "%s", s->title);
last_state = s->state;
} else { last_app[0] = 0; last_title[0] = 0; last_state = 0xFFFFFFFFu; }
need_redraw = 1;
}
/* 空态/无源:立即重画(清占位/回空态) */
if (!s || s->state == MEOW_MEDIA_STOPPED) need_redraw = 1;
if (need_redraw) {
np_draw_page(0);
meow_dirty_add(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H);
meow_ui_flush();
need_redraw = 0;
last_prog = now;
} else if (s && (now - last_prog >= 1000)) {
/* 进度推进:仅重绘进度带 + 按钮 */
last_prog = now;
np_repaint_progress();
}
usleep(10000);
}
if (tfd >= 0) close(tfd);
kp_close(&kp_keys);
if (g_np_cover) { meow_img_free(g_np_cover); g_np_cover = NULL; }
g_np_cover_path[0] = 0;
}
+154
View File
@@ -0,0 +1,154 @@
/* test_ipc_harness.c — 宿主 IPC 服务端模拟(等价 launcher ipc_dispatch 的媒体子集).
*
* 用 meow_media.c + meow_ipc.h 复刻 launcher 的 IPC 服务器对媒体消息的处理,
* 供「qemu-mipsel meow_ipc listen/meta/playctl」端到端验证:
* listen → HELLO(MF_PLAYBACK)+META 注册源并等待下发;
* meta → 上报 META(作为新源/更新);
* playctl → 上传控制,服务端转发给「最近活跃会话」fd。
* 运行:gcc test_ipc_harness.c meow_media.c -o /tmp/meow_srv ; /tmp/meow_srv /tmp/e2e.sock
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <poll.h>
#include "meow_ipc.h"
#include "meow_media.h"
#define MAXCLI 8
typedef struct {
int fd;
uint8_t hdr[MEOW_IPC_HDR_LEN]; int hdr_n;
uint8_t pay[MEOW_IPC_MAX]; uint32_t pay_tot, pay_n;
char app_id[MEOW_APP_ID_MAX];
} conn;
static conn g_c[MAXCLI];
static size_t srv_send(int fd, uint16_t type, uint32_t req, const void *payload, uint32_t len)
{
if (fd < 0) return 0;
uint8_t h[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION;
memcpy(h+0,&magic,4); memcpy(h+4,&ver,2); memcpy(h+6,&type,2); memcpy(h+8,&req,4); memcpy(h+12,&len,4);
size_t off=0;
while (off<sizeof h){ ssize_t w=write(fd,h+off,sizeof h-off); if(w<=0)return off; off+=(size_t)w; }
if(len&&payload){ size_t p=0; while(p<len){ ssize_t w=write(fd,(const uint8_t*)payload+p,len-p); if(w<=0)break; p+=(size_t)w; } off+=p; }
return off;
}
static conn *find_slot(int fd) { for (int i=0;i<MAXCLI;i++) if (g_c[i].fd==fd) return &g_c[i]; return NULL; }
static conn *alloc_slot(int fd){ for (int i=0;i<MAXCLI;i++) if (g_c[i].fd==-1){ g_c[i].fd=fd; g_c[i].hdr_n=0; g_c[i].pay_tot=g_c[i].pay_n=0; g_c[i].app_id[0]=0; return &g_c[i]; } return NULL; }
static void handle(conn *c)
{
uint16_t type; uint32_t req;
memcpy(&type, c->hdr+6, 2); memcpy(&req, c->hdr+8, 4);
switch (type) {
case MSG_HELLO: {
m_hello_t h; memset(&h,0,sizeof h); memcpy(&h,c->pay,c->pay_n<sizeof h?c->pay_n:sizeof h);
snprintf(c->app_id, sizeof c->app_id, "%s", h.app_id);
m_ack_t a; a.code=MEOW_OK; a.status=1; srv_send(c->fd,MSG_ACK,req,&a,sizeof a);
printf("SRV: HELLO app=%s flags=%u -> ACK\n", c->app_id, h.flags); break;
}
case MSG_META: {
m_meta_t m; memset(&m,0,sizeof m); memcpy(&m,c->pay,c->pay_n<sizeof m?c->pay_n:sizeof m);
m.app_id[MEOW_APP_ID_MAX-1]=0; m.title[127]=0; m.artist[127]=0; m.album[127]=0; m.artwork[191]=0; m.lyric[191]=0;
int rc = meow_media_meta(&m, c->fd);
m_ack_t a; a.code=MEOW_OK; a.status=(uint32_t)rc; srv_send(c->fd,MSG_ACK,req,&a,sizeof a);
const meow_media_session_t *cur = meow_media_current();
printf("SRV: META app=%s state=%u rc=%d | current=%s\n", m.app_id, m.state, rc,
cur?cur->app_id:"(none)");
break;
}
case MSG_PLAYCTL: {
m_playctl_t p; memset(&p,0,sizeof p); memcpy(&p,c->pay,c->pay_n<sizeof p?c->pay_n:sizeof p);
int rc = meow_media_playctl(p.cmd, p.seek_ms);
m_ack_t a; a.code=MEOW_OK; a.status=(uint32_t)rc; srv_send(c->fd,MSG_ACK,req,&a,sizeof a);
printf("SRV: PLAYCTL cmd=%u seek=%lld rc=%d (target current)\n", p.cmd, (long long)p.seek_ms, rc);
break;
}
default: {
m_err_t e; memset(&e,0,sizeof e); e.code=MEOW_ERR_INVAL; e.msg_len=5; memcpy(e.msg,"nope",5);
srv_send(c->fd,MSG_ERR,req,&e,sizeof e);
printf("SRV: unsupported type 0x%04x\n", type); break;
}
}
}
/* 读一个连接可用字节并组装帧(>=1 完整帧即 dispatch)。返回 0=ok,-1=连接关闭。 */
static int conn_read(conn *c)
{
for (;;) {
if ((unsigned)c->hdr_n < MEOW_IPC_HDR_LEN) {
ssize_t r = read(c->fd, c->hdr + c->hdr_n, MEOW_IPC_HDR_LEN - c->hdr_n);
if (r < 0) { if (errno==EAGAIN) return 0; return -1; }
if (r == 0) return -1;
c->hdr_n += (int)r;
if ((unsigned)c->hdr_n < MEOW_IPC_HDR_LEN) return 0;
uint32_t magic, len; uint16_t ver;
memcpy(&magic,c->hdr,4); memcpy(&ver,c->hdr+4,2); memcpy(&len,c->hdr+12,4);
if (magic!=MEOW_IPC_MAGIC||ver!=MEOW_IPC_VERSION||len>MEOW_IPC_MAX) return -1;
c->pay_tot=len; c->pay_n=0;
if (len==0){ handle(c); c->hdr_n=0; continue; }
}
if (c->pay_n < c->pay_tot) {
ssize_t r = read(c->fd, c->pay + c->pay_n, c->pay_tot - c->pay_n);
if (r < 0){ if (errno==EAGAIN) return 0; return -1; }
if (r==0) return -1;
c->pay_n += (uint32_t)r;
if (c->pay_n < c->pay_tot) return 0;
}
handle(c);
c->hdr_n=0; c->pay_n=0;
}
}
int main(int argc, char **argv)
{
const char *path = (argc>1)?argv[1]:"/tmp/meow_ipc_e2e.sock";
for (int i=0;i<MAXCLI;i++) g_c[i].fd=-1;
meow_media_ops ops;
ops.send=srv_send; ops.log=NULL; meow_media_init(&ops);
int lfd=socket(AF_UNIX,SOCK_STREAM,0);
unlink(path);
struct sockaddr_un a; memset(&a,0,sizeof a); a.sun_family=AF_UNIX; strncpy(a.sun_path,path,sizeof a.sun_path-1);
if (bind(lfd,(struct sockaddr*)&a,sizeof a)<0){ perror("bind"); return 1; }
chmod(path,0666);
if (listen(lfd,8)<0){ perror("listen"); return 1; }
fcntl(lfd,F_SETFL,O_NONBLOCK);
printf("SRV: listening %s\n", path); fflush(stdout);
struct pollfd fds[MAXCLI+1]; int nfds=1;
fds[0].fd=lfd; fds[0].events=POLLIN; fds[0].revents=0;
while (1) {
fds[0].fd=lfd; fds[0].events=POLLIN;
int n=1;
for (int i=0;i<MAXCLI;i++) if (g_c[i].fd>=0){ fds[n].fd=g_c[i].fd; fds[n].events=POLLIN; fds[n].revents=0; n++; }
nfds=n;
int r=poll(fds,nfds,400);
if (r<0){ if(errno==EINTR)continue; break; }
if (fds[0].revents&POLLIN){
for(;;){ int c=accept(lfd,NULL,NULL); if(c<0)break; fcntl(c,F_SETFL,O_NONBLOCK);
if(!alloc_slot(c)){ close(c); printf("SRV: reject conn (full)\n"); } else printf("SRV: accept fd=%d\n", c); }
}
for (int i=1;i<nfds;i++){
int ev=fds[i].revents;
if (ev&(POLLIN|POLLHUP|POLLERR|POLLNVAL)){
conn *c=find_slot(fds[i].fd);
if (!c) continue;
if (conn_read(c)<0){ printf("SRV: conn %d closed\n", c->fd); close(c->fd); c->fd=-1; }
}
}
fflush(stdout);
}
close(lfd); unlink(path);
return 0;
}
+169
View File
@@ -0,0 +1,169 @@
/* test_media.c — 宿主单测:meow_media 媒体会话(阶段D附录B)。
*
* 验证:
* 1) META 上报 → 注册会话,current()=最近上报者;
* 2) 最近活跃切换(两个源,后上报者胜);
* 3) has_source()(state=0 停止 → 0);
* 4) PLAYCTL 下发字节格式(socketpair 对端可读回 MSG_PLAYCTL 帧,cmd/seek 正确);
* 5) meow_media_playctl 无源 → -1;
* 6) clear(app) 清会话 → current()=NULL。
* 只依赖 meow_media.c + meow_ipc.h(纯 C),宿主 gcc 直编。
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/socket.h>
#include "meow_media.h"
#include "meow_ipc.h"
static int g_fails = 0;
#define CHECK(cond, msg) do { \
if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
/* ops.send:把帧写到 fd(socketpair 一端)。 */
static size_t test_send(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
size_t off = 0;
while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) return off;
off += (size_t)w;
}
if (len && payload) {
size_t p = 0;
while (p < len) {
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
if (w <= 0) break;
p += (size_t)w;
}
off += p;
}
return off;
}
static void fill_meta(m_meta_t *m, const char *app, uint32_t state, uint32_t pos, uint32_t dur,
const char *title, const char *artist)
{
memset(m, 0, sizeof *m);
snprintf(m->app_id, sizeof m->app_id, "%s", app);
m->state = state; m->pos_ms = pos; m->dur_ms = dur;
snprintf(m->title, sizeof m->title, "%s", title);
snprintf(m->artist, sizeof m->artist, "%s", artist);
}
/* 从 fd 读一帧(头+负载),校验 magic/ver,返回负载类型;不足返回 -1。 */
static int read_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *rbuf, uint32_t rcap)
{
uint8_t rhdr[MEOW_IPC_HDR_LEN];
size_t off = 0;
while (off < sizeof rhdr) {
ssize_t r = read(fd, rhdr + off, sizeof rhdr - off);
if (r <= 0) return -1;
off += (size_t)r;
}
uint32_t magic, len; uint16_t ver;
memcpy(&magic, rhdr, 4); memcpy(&ver, rhdr + 4, 2); memcpy(type, rhdr + 6, 2);
memcpy(req_id, rhdr + 8, 4); memcpy(&len, rhdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
if (len > rcap) len = rcap;
off = 0;
while (off < len) {
ssize_t r = read(fd, rbuf + off, len - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return (int)len;
}
int main(void)
{
meow_media_ops ops;
ops.send = test_send;
ops.log = NULL; /* 静默 */
meow_media_init(&ops);
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) { perror("socketpair"); return 1; }
/* sv[0]=会话 fd(launcher 下发端);sv[1]=测试读回端(模拟 App)。 */
/* 1) 单源注册 */
m_meta_t m;
fill_meta(&m, "com.ncm", MEOW_MEDIA_PLAYING, 120000, 240000, "Song A", "Artist A");
CHECK(meow_media_meta(&m, sv[0]) == 0, "meta registers source");
const meow_media_session_t *cur = meow_media_current();
CHECK(cur && !strcmp(cur->app_id, "com.ncm"), "current() == com.ncm");
CHECK(cur && cur->state == MEOW_MEDIA_PLAYING, "state == PLAYING");
CHECK(meow_media_has_source() == 1, "has_source == 1 (playing)");
/* 2) 多源最近活跃切换 */
fill_meta(&m, "com.player2", MEOW_MEDIA_PLAYING, 0, 180000, "Song B", "Artist B");
CHECK(meow_media_meta(&m, sv[0]) == 0, "meta registers second source");
cur = meow_media_current();
CHECK(cur && !strcmp(cur->app_id, "com.player2"), "current() == com.player2 (newest)");
fill_meta(&m, "com.ncm", MEOW_MEDIA_PLAYING, 30000, 240000, "Song A", "Artist A");
meow_media_meta(&m, sv[0]);
cur = meow_media_current();
CHECK(cur && !strcmp(cur->app_id, "com.ncm"), "current() == com.ncm after re-report (newest)");
/* 3) state=0 停止 → has_source=0(但会话仍在,current() 仍指向它) */
fill_meta(&m, "com.ncm", MEOW_MEDIA_STOPPED, 0, 0, "Song A", "Artist A");
meow_media_meta(&m, sv[0]);
cur = meow_media_current();
CHECK(cur && !strcmp(cur->app_id, "com.ncm"), "current() still == com.ncm (stopped)");
CHECK(meow_media_has_source() == 0, "has_source == 0 (stopped)");
/* 恢复播放,准备 PLAYCTL 下发验证 */
fill_meta(&m, "com.ncm", MEOW_MEDIA_PLAYING, 1000, 240000, "Song A", "Artist A");
meow_media_meta(&m, sv[0]);
/* 4) PLAYCTL 下发:cmd=1(播放),读回端应收到 MSG_PLAYCTL/第11号 req */
CHECK(meow_media_playctl(1, 0) == 0, "playctl sends (cmd=play)");
uint8_t rbuf[512];
uint16_t type; uint32_t req;
int n = read_frame(sv[1], &type, &req, rbuf, sizeof rbuf);
CHECK(n == (int)sizeof(m_playctl_t), "downlink frame payload == m_playctl_t size");
CHECK(type == MSG_PLAYCTL, "downlink type == MSG_PLAYCTL");
m_playctl_t c;
memcpy(&c, rbuf, sizeof c);
CHECK(c.cmd == 1 && c.seek_ms == 0, "downlink cmd=1 seek=0");
/* 5) seek:cmd=5 seek=12345 */
CHECK(meow_media_playctl(5, 12345) == 0, "playctl sends (cmd=seek)");
n = read_frame(sv[1], &type, &req, rbuf, sizeof rbuf);
memcpy(&c, rbuf, sizeof c);
CHECK(type == MSG_PLAYCTL && c.cmd == 5 && c.seek_ms == 12345, "downlink seek cmd=5 seek=12345");
/* playctl_to 目标 app(另一会话) */
CHECK(meow_media_playctl_to("com.player2", 3, 0) == 0, "playctl_to player2 sends");
n = read_frame(sv[1], &type, &req, rbuf, sizeof rbuf);
memcpy(&c, rbuf, sizeof c);
CHECK(type == MSG_PLAYCTL && c.cmd == 3, "downlink to player2 cmd=3");
CHECK(meow_media_playctl_to("no.such.app", 1, 0) == -1, "playctl_to unknown app == -1");
/* 6) 无源:清全部 → playctl -1 / has_source 0 */
meow_media_clear(NULL);
CHECK(meow_media_current() == NULL, "current() == NULL after clear all");
CHECK(meow_media_playctl(1, 0) == -1, "playctl no source == -1");
CHECK(meow_media_has_source() == 0, "has_source == 0 after clear");
close(sv[0]); close(sv[1]);
/* 7) 空 app_id 拒绝 */
fill_meta(&m, "", MEOW_MEDIA_PLAYING, 0, 0, "x", "y");
CHECK(meow_media_meta(&m, sv[0]) == -1, "meta empty app_id == -1");
printf("\n%s (%d fails)\n", g_fails ? "FAILED" : "ALL PASS", g_fails);
return g_fails ? 1 : 0;
}
+47
View File
@@ -862,6 +862,14 @@ static size_t ipc_send(int fd, uint16_t type, uint32_t req_id,
} }
/* 公开的 ipc_send 包装(media 模块经 ops 注入用;签名与 meow_media_ops.send 一致)。 */
size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len)
{
return ipc_send(fd, type, req_id, payload, len);
}
/* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */ /* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */
static void ipc_enqueue_frame(struct meow_ipc_conn *c) static void ipc_enqueue_frame(struct meow_ipc_conn *c)
{ {
@@ -1014,6 +1022,12 @@ static void *ipc_thread(void *arg)
* (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */ * (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */
int meow_ipc_init(void) int meow_ipc_init(void)
{ {
/* 注入媒体会话 ops:send=ipc_send 包装、log=logmsg(阶段D附录B)。 */
static meow_media_ops g_media_ops;
g_media_ops.send = meow_ipc_send_pub;
g_media_ops.log = logmsg;
meow_media_init(&g_media_ops);
ipc_event_fd = eventfd(0, EFD_NONBLOCK); ipc_event_fd = eventfd(0, EFD_NONBLOCK);
if (ipc_event_fd < 0) if (ipc_event_fd < 0)
logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno)); logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno));
@@ -1246,6 +1260,39 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
* 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */ * 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */
logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here"); logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here");
break; break;
/* 阶段D附录B:媒体会话(FRAMEWORK_DESIGN 附录 B)。META 声明 MF_PLAYBACK 的 App 上报
* 元数据/进度/状态 → 归并到 meow_media 会话(最近活跃=当前源);回 ACK。 */
case MSG_META: {
m_meta_t mm;
memset(&mm, 0, sizeof mm);
memcpy(&mm, m->payload, m->plen < (uint16_t)sizeof mm ? m->plen : (uint16_t)sizeof mm);
mm.app_id[MEOW_APP_ID_MAX - 1] = 0;
mm.title[127] = 0; mm.artist[127] = 0; mm.album[127] = 0;
mm.artwork[191] = 0; mm.lyric[191] = 0;
int rc = meow_media_meta(&mm, m->fd);
m_ack_t a;
a.code = MEOW_OK; /* 会话策略:槽满降级最旧,不视为错误 */
a.status = (uint32_t)rc;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: META app=%s state=%u pos=%u/%u rc=%d -> ACK",
mm.app_id, mm.state, mm.pos_ms, mm.dur_ms, rc);
break;
}
/* MSG_PLAYCTL(↑ 控制器→launcher 转发):把 cmd/seek 下发到「最近活跃媒体会话」fd。
* 物理媒体键 / 宿主验证经此外发;通知中心卡片/播放页按钮走 meow_media_playctl 直发。 */
case MSG_PLAYCTL: {
m_playctl_t c;
memset(&c, 0, sizeof c);
memcpy(&c, m->payload, m->plen < (uint16_t)sizeof c ? m->plen : (uint16_t)sizeof c);
int rc = meow_media_playctl(c.cmd, c.seek_ms);
m_ack_t a;
a.code = MEOW_OK;
a.status = (uint32_t)rc; /* 0=已下发;-1=无源/发送失败(status=0xFFFFFFFF) */
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: PLAYCTL cmd=%u seek=%lld rc=%d -> ACK", c.cmd,
(long long)c.seek_ms, rc);
break;
}
default: default:
logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id); logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id);
{ {