diff --git a/patch_v2/meow_launcherd/app_launcher.c b/patch_v2/meow_launcherd/app_launcher.c index a8db6c2..16eb6b4 100644 --- a/patch_v2/meow_launcherd/app_launcher.c +++ b/patch_v2/meow_launcherd/app_launcher.c @@ -1301,6 +1301,8 @@ static int launch_app_chain(struct app *a, const char *chain) * 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */ settings_load(); meow_nav_exit(); /* M3:前台应用会话结束,清导航栈 */ + /* 阶段D附录B:源 App 退出 → 清其媒体会话(如该 App 上报过 META),通知中心/播放页回空态 */ + meow_media_clear(a->id); logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status)); return 0; } diff --git a/patch_v2/meow_launcherd/build_launcher.sh b/patch_v2/meow_launcherd/build_launcher.sh index 180ef06..2e4434c 100755 --- a/patch_v2/meow_launcherd/build_launcher.sh +++ b/patch_v2/meow_launcherd/build_launcher.sh @@ -25,9 +25,9 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf" "$CC" -I"../sdk" -Os -static \ 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 \ - 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 \ - 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 echo "== meow_launcherd ==" ls -la meow_launcherd diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c index 139bbec..d64825c 100644 --- a/patch_v2/meow_launcherd/main_menu.c +++ b/patch_v2/meow_launcherd/main_menu.c @@ -825,6 +825,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, /* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */ meow_ipc_process(); meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/ + meow_media_tick(); /* 阶段D附录B:媒体源死亡(播放中无 META 超时)清槽回空态 */ /* sim hooks */ 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) { 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; @@ -1296,7 +1305,31 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, continue; /* 本圈不再按 tap/其余处理 */ } /* 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) { logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy); } else if (tx >= 0 && ty >= 0) { diff --git a/patch_v2/meow_launcherd/meow_ipc b/patch_v2/meow_launcherd/meow_ipc index 9c89700..7f6d15d 100755 Binary files a/patch_v2/meow_launcherd/meow_ipc and b/patch_v2/meow_launcherd/meow_ipc differ diff --git a/patch_v2/meow_launcherd/meow_ipc.c b/patch_v2/meow_launcherd/meow_ipc.c index 81ed824..c2e5206 100644 --- a/patch_v2/meow_launcherd/meow_ipc.c +++ b/patch_v2/meow_launcherd/meow_ipc.c @@ -342,6 +342,126 @@ static int cmd_dismissall(void) return 1; } +/* 阶段 D 附录 B:媒体会话上报 → MSG_META,期望 MSG_ACK。 + * meta <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) { const char *env = getenv("MEOW_IPC_SOCK"); @@ -408,6 +528,30 @@ int main(int argc, char **argv) if (!strcmp(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")) { /* 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; } diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index 43da2b8..5247360 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -13,8 +13,9 @@ * uint32_t len; = payload 字节数 * }; * - * 本阶段(D · M0+M1+M2+M3 前置)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT + - * MSG_ACT_FOCUS/BACK + MSG_NOTIFY/PROGRESS/QUERY/DISMISS* + MSG_NAV(导航栈上报/退出)。 + * 本阶段(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_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)。 * picker(0x0070/0x0071)仅预留协议位,本批不实现。 */ #ifndef MEOW_IPC_H @@ -67,6 +68,8 @@ enum { * 待 M3 延伸阶段定义(m_picker_call_t / m_picker_result_t),此处仅固定消息号与方向。 */ MSG_PICKER_CALL = 0x0070, /* ↑ 请求跨 App picker 调用(发起方 A → 持有方 B) */ MSG_PICKER_RESULT = 0x0071, /* ↓ picker 返回值(候选结果回 A) */ + MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */ + MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */ MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */ }; @@ -90,6 +93,11 @@ enum { #define MF_BACKGROUND 0x00000001u /* 可后台常驻 */ #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_ACT_MAX 64 @@ -164,6 +172,36 @@ typedef struct m_nav { char current[MEOW_ACT_MAX]; /* REPORT: 当前顶层 activity_id;EXIT: 可空 */ } 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)。 * - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon` * (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退); diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index b01c700..e40712e 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -36,6 +36,7 @@ extern "C" { #include "meow_ipc.h" /* M0+M1:IPC 协议帧/消息/负载定(与 meow_ipc 工具同源) */ #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_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_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */ #include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */ @@ -686,6 +687,8 @@ void restore_backlight(void); uint16_t rgb565(uint32_t c); void run_notif_center(struct app *apps, int napps, notif_click_t *click, int live_open, int lox, int loy); +/* 阶段D附录B:Now Playing 大页(主界面横划进入;左缘右滑返回;阻塞到返回)。 */ +void run_now_playing(void); void run_settings_page(void); void s_about_open(void); int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur); diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 969f0f3..d9bf63b 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_media.c b/patch_v2/meow_launcherd/meow_media.c new file mode 100644 index 0000000..9485aea --- /dev/null +++ b/patch_v2/meow_launcherd/meow_media.c @@ -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); + } +} diff --git a/patch_v2/meow_launcherd/meow_media.h b/patch_v2/meow_launcherd/meow_media.h new file mode 100644 index 0000000..9c0133f --- /dev/null +++ b/patch_v2/meow_launcherd/meow_media.h @@ -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 */ diff --git a/patch_v2/meow_launcherd/notif_center.c b/patch_v2/meow_launcherd/notif_center.c index 59ef49b..a30bd2a 100644 --- a/patch_v2/meow_launcherd/notif_center.c +++ b/patch_v2/meow_launcherd/notif_center.c @@ -26,6 +26,34 @@ static int g_ncp_swipe_row = -1; 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 + 截断) */ 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) { const meow_ui_theme_t *th = s_ui_theme(); + int lt = ncp_list_top(), lv = ncp_list_view(); 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; } 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++) { - int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll; - if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */ + int ry = lt + i * NCP_ROW_H - scroll; + if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */ int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */ if (off < 0) ncp_draw_delete_bg(ry, off, th); ncp_draw_row(i, ry, off, th); } /* 滚动条 */ - if (total > NCP_LIST_BOT - NCP_LIST_TOP) { - int view = NCP_LIST_BOT - NCP_LIST_TOP; - int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24; - int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view); - meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 3, th->surface_container); + if (total > lv) { + int thumb_h = lv * lv / total; if (thumb_h < 24) thumb_h = 24; + int ty = lt + (lv - thumb_h) * scroll / (total - lv); + meow_gfx_fill_round(fb_xres - 8, lt, 5, lv, 3, th->surface_container); 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) { 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); - 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(); } @@ -475,8 +504,9 @@ static void ncp_repaint_row(int scroll, int row, int off) { const meow_ui_theme_t *th = s_ui_theme(); if (row < 0 || row >= g_nrows) return; - int ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll; - if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */ + int lt = ncp_list_top(); + int ry = lt + row * NCP_ROW_H - scroll; + if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */ /* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */ 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); @@ -577,6 +607,139 @@ static int ncp_clear_all(void) /* 绘制整个通知中心面板(画进 SDK back_buf;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) { 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); + /* 阶段D附录B:顶部媒体卡片(活动媒体会话时显示,固定于列表上方;列表可视区随之下移) */ + ncp_draw_media_card(th); + /* 通知列表 */ 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) { (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); if (row < 0 || row >= g_nrows) return 0; 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→FIX 滑入。 */ 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(); if (g_ncp_ph > 0) { anim_ok = 1; @@ -869,6 +1035,8 @@ void run_notif_center(struct app *apps, int napps, notif_click_t *click, anim_last = now_ms(); int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */ + int media_prev = -1; /* 阶段D附录B:上次媒体卡片显示态(检测 出现/消失 → 整面板重排) */ + int media_paint = 0; /* 阶段D附录B:仅重绘媒体卡片带(进度/封面变化,局部刷新) */ for (;;) { 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 adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy; 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); /* 起点必须在列表区(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_swipe_row = row; 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); if (ady2 >= S_SCROLL_SLOP) { int av = g_nrows * NCP_ROW_H; - int vw = NCP_LIST_BOT - NCP_LIST_TOP; - int in_over = (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && + int vw = ncp_list_view(); + int in_over = (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT && mnp_scroll_max(av, vw) > 0); if (!in_over) { 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; if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { 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 && - tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT) { + tdy >= ncp_list_top() && tdy < NCP_LIST_BOT) { scroll_drag = 1; meow_settings_get_bool("input.swipe_invert", &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) { 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); list_scroll = mnp_scroll_clamp(target, 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 一旦置位即跳过 * 下方 mnp_vertical_up 关闭判定。 */ 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); list_scroll = mnp_scroll_clamp(target, total, view); 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; ncp_refresh_rows(); /* 按 dnd 过滤重采集 */ 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; } /* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 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; ncp_refresh_rows(); 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; } - 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)) { hit = 1; 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;变化重采集 + 重绘列表区 */ if (now - last_listchk >= 500) { last_listchk = now; + /* 阶段D附录B:面板驻留时排空 IPC(App 上报 META 更新媒体会话)+ 媒体会话 TTL,再做展示刷新 */ + meow_ipc_process(); 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()) { /* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */ 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; ncp_refresh_rows(); list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, - NCP_LIST_BOT - NCP_LIST_TOP); + ncp_list_view()); need_paint = 1; } } + if (media_paint) { ncp_repaint_media_card(); media_paint = 0; } /* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。 * 每 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; ncp_refresh_rows(); list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, - NCP_LIST_BOT - NCP_LIST_TOP); + ncp_list_view()); need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */ } } diff --git a/patch_v2/meow_launcherd/now_playing.c b/patch_v2/meow_launcherd/now_playing.c new file mode 100644 index 0000000..dbff86e --- /dev/null +++ b/patch_v2/meow_launcherd/now_playing.c @@ -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; +} diff --git a/patch_v2/meow_launcherd/test_ipc_harness.c b/patch_v2/meow_launcherd/test_ipc_harness.c new file mode 100644 index 0000000..51ac896 --- /dev/null +++ b/patch_v2/meow_launcherd/test_ipc_harness.c @@ -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; +} diff --git a/patch_v2/meow_launcherd/test_media.c b/patch_v2/meow_launcherd/test_media.c new file mode 100644 index 0000000..0e5a44a --- /dev/null +++ b/patch_v2/meow_launcherd/test_media.c @@ -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; +} diff --git a/patch_v2/meow_launcherd/util.c b/patch_v2/meow_launcherd/util.c index 0100474..933e472 100644 --- a/patch_v2/meow_launcherd/util.c +++ b/patch_v2/meow_launcherd/util.c @@ -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 发布)。 */ 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 仍安全轮询空队列。) */ 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); if (ipc_event_fd < 0) 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 通知中心接入)。 */ logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here"); 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: logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id); {