/* meow_ui_media.c — App 侧媒体会话封装(SDK 公共件;见 meow_ui_media.h)。 * * 传输:AF_UNIX SOCK_STREAM 到 MEOW_IPC_SOCK(缺省 /usr/data/meow_ipc.sock)。后台线程持有 * 持久连接:HELLO(app_id, MF_PLAYBACK) 注册为媒体源 + poll 读 launcher 下发 MSG_PLAYCTL(经 * 该连接 fd 回发,见 launcher meow_media.c send_playctl)。meow_ui_media_meta() 在调用线程 * 直接经同一连接发 MSG_META(全双工:写 META、读 PLAYCTL 互不干扰)。 * * 连接生命周期(best-effort):懒连(首次 meta / 注册回调时 connect+HELLO+ACK); * 断线(读失败/写失败)→ close+置 -1,下次 meta 重连;launcher 不在 → connect 失败 → 静默。 * 线程安全:g_fd 读写经互斥;后台线程只在 poll/读失败时改 fd,meta 线程只在 connect 时改 fd。 */ #include "meow_ui_media.h" #include "meow_ipc.h" #include #include #include #include #include #include #include #include #include static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static char g_app[MEOW_APP_ID_MAX] = ""; static int g_app_set = 0; static int g_fd = -1; static pthread_t g_thr; static int g_thr_on = 0; static volatile int g_quit = 0; static meow_media_playctl_fn g_cb = NULL; /* 本进程最后一次经 meow_ui_media_meta 上报的 state(0=停 1=播放 2=暂停 3=缓冲)。 * 供「物理播放/暂停键」做切换判据:播放中→暂停(2),否则→播放(1)。与 launcher * now_playing.c 的同款判定(state==PLAYING ? 2 : 1)对齐。 */ static int g_last_state = 0; #define MEOW_UI_MEDIA_STATE_PLAYING 1 /* 后台线程主体(定义在文件后段;on_playctl 懒起时引用)。 */ static void *media_thread_main(void *arg); static const char *sock_path(void) { const char *e = getenv("MEOW_IPC_SOCK"); return (e && e[0]) ? e : MEOW_IPC_PATH; } static int connect_path(const char *path) { int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return -1; 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 (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } return fd; } /* 写满 n 字节(EINTR 重试);失败返回 0。 */ static int write_all(int fd, const uint8_t *p, size_t n) { size_t off = 0; while (off < n) { ssize_t w = write(fd, p + off, n - off); if (w < 0) { if (errno == EINTR) continue; return 0; } if (w == 0) return 0; off += (size_t)w; } return 1; } static int read_all(int fd, uint8_t *p, size_t n) { size_t off = 0; while (off < n) { ssize_t r = read(fd, p + off, n - off); if (r < 0) { if (errno == EINTR) continue; return 0; } if (r == 0) return 0; off += (size_t)r; } return 1; } /* 组装并写出 16B(LE)帧头 + 负载。 */ static int send_frame(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); if (!write_all(fd, hdr, sizeof hdr)) return -1; if (len && payload && !write_all(fd, payload, len)) return -1; return 0; } /* 读一帧头+负载;返回负载长(≥0),-1=断/坏帧。 */ static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap) { uint8_t hdr[MEOW_IPC_HDR_LEN]; if (!read_all(fd, hdr, sizeof hdr)) return -1; uint32_t magic, len; uint16_t ver; memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2); memcpy(&len, hdr + 12, 4); if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1; if (len > rcap) len = rcap; if (len > 0 && !read_all(fd, rbuf, len)) return -1; return (int)len; } static void media_app_id(void) { if (g_app_set) return; const char *s = getenv("MEOW_LOG_APP"); snprintf(g_app, sizeof g_app, "%s", (s && s[0]) ? s : "app"); g_app_set = 1; } /* (调用方已持 mu)连接 + HELLO(MF_PLAYBACK) + 读 ACK。成功存 g_fd 返回 fd;失败 -1。 */ static int conn_locked(void) { if (g_fd >= 0) return g_fd; int fd = connect_path(sock_path()); if (fd < 0) return -1; media_app_id(); m_hello_t h; memset(&h, 0, sizeof h); snprintf(h.app_id, sizeof h.app_id, "%s", g_app); h.version = 1; h.flags = MF_PLAYBACK; if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) != 0) { close(fd); return -1; } uint8_t rbuf[256]; uint16_t rt; if (read_frame(fd, &rt, rbuf, sizeof rbuf) < 0 || (rt != MSG_ACK && rt != MSG_ERR)) { close(fd); return -1; } g_fd = fd; return fd; } /* (调用方已持 mu)标记断线并关闭。 */ static void mark_dead_locked(void) { if (g_fd >= 0) { close(g_fd); g_fd = -1; } } void meow_ui_media_init(const char *app_id) { pthread_mutex_lock(&g_mu); if (app_id && app_id[0]) { snprintf(g_app, sizeof g_app, "%s", app_id); g_app_set = 1; } else if (!g_app_set) { const char *s = getenv("MEOW_LOG_APP"); snprintf(g_app, sizeof g_app, "%s", (s && s[0]) ? s : "app"); g_app_set = 1; } pthread_mutex_unlock(&g_mu); } void meow_ui_media_on_playctl(meow_media_playctl_fn cb) { pthread_mutex_lock(&g_mu); g_cb = cb; int need_start = !g_thr_on; if (need_start) { g_quit = 0; g_thr_on = 1; } pthread_mutex_unlock(&g_mu); if (need_start) pthread_create(&g_thr, NULL, media_thread_main, NULL); /* detach:App 退出经 meow_ui_media_stop 设 g_quit 后线程自身退出,无需 join */ if (need_start) pthread_detach(g_thr); } void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms, const char *title, const char *artist, const char *album, const char *artwork, const char *lyric) { m_meta_t mm; memset(&mm, 0, sizeof mm); pthread_mutex_lock(&g_mu); media_app_id(); g_last_state = (int)state; /* 记录最近上报 state:供物理播放/暂停键切换判据(见 sys_key) */ snprintf(mm.app_id, sizeof mm.app_id, "%s", g_app); mm.state = state; mm.pos_ms = pos_ms; mm.dur_ms = dur_ms; if (title) snprintf(mm.title, sizeof mm.title, "%s", title); if (artist) snprintf(mm.artist, sizeof mm.artist, "%s", artist); if (album) snprintf(mm.album, sizeof mm.album, "%s", album); if (artwork && artwork[0]) { snprintf(mm.artwork, sizeof mm.artwork, "%s", artwork); mm.flags |= MF_ART | MF_COVER; } if (lyric && lyric[0]) { snprintf(mm.lyric, sizeof mm.lyric, "%s", lyric); mm.flags |= MF_LYRIC; } int fd = conn_locked(); if (fd >= 0) { if (send_frame(fd, MSG_META, (uint32_t)((uint64_t)pos_ms & 0x7FFFFFFF), &mm, sizeof mm) != 0) { mark_dead_locked(); /* 断线:下次 meta 重连 */ fd = conn_locked(); if (fd >= 0) (void)send_frame(fd, MSG_META, (uint32_t)((uint64_t)pos_ms & 0x7FFFFFFF), &mm, sizeof mm); } } pthread_mutex_unlock(&g_mu); } void meow_ui_media_stop(void) { pthread_mutex_lock(&g_mu); g_quit = 1; mark_dead_locked(); g_thr_on = 0; pthread_mutex_unlock(&g_mu); } /* ---- 物理媒体键 → 系统语义(M2.xx:SDK 输入层接管,App 前台也生效)---- * 物理键是系统资源(APPS.md §3.4.1):播放/暂停/上一曲/下一曲 → 转发到当前媒体会话。 * 本进程注册过 playctl 回调(= 本 App 是媒体源)→ 直接本地回调(最简,无 IPC 往返); * 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到「最近活跃媒体会话」。best-effort 绝不崩。 */ /* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek)。 * 播放/暂停(164)按最近上报 state 切换(播放中→暂停,否则→播放);上一曲 165/19→3,下一曲 163→4。 * 非媒体键返回 -1。last_state 由调用方在持锁下传入(读 g_last_state 快照)。 */ static int media_key_cmd(int code, int last_state) { switch (code) { case 164: /* KEY_PLAYPAUSE:切换(依最近上报 state) */ return (last_state == MEOW_UI_MEDIA_STATE_PLAYING) ? 2 : 1; case 165: case 19: /* KEY_PREVIOUSSONG / 旧码 */ return 3; case 163: /* KEY_NEXTSONG */ return 4; default: return -1; } } /* 控制器路径:发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话(launcher ipc_dispatch * MSG_PLAYCTL → meow_media_playctl → 下发到当前源 fd)。 * 专门开一条临时连接(不复用媒体源持久连接 g_fd,避免与后台线程 poll/读竞争); * 无 launcher/断线 → 静默降级(绝不崩)。 */ static void send_playctl_upstream(int cmd, int64_t seek_ms) { int fd = connect_path(sock_path()); if (fd < 0) return; m_playctl_t c; memset(&c, 0, sizeof c); c.cmd = (uint32_t)cmd; c.seek_ms = seek_ms; if (send_frame(fd, MSG_PLAYCTL, 1, &c, sizeof c) != 0) { close(fd); return; } /* 读 ACK(best-effort):带 200ms poll,防 launcher 挂死导致输入线程阻塞。 */ struct pollfd p; p.fd = fd; p.events = POLLIN; p.revents = 0; if (poll(&p, 1, 200) > 0 && (p.revents & POLLIN)) { uint8_t rbuf[128]; uint16_t rt; (void)read_frame(fd, &rt, rbuf, sizeof rbuf); } close(fd); } /* 物理媒体键按下边沿(val==1 时由输入线程调用;重复/抬起不重复触发)。 */ void meow_ui_media_sys_key(int code) { pthread_mutex_lock(&g_mu); meow_media_playctl_fn cb = g_cb; int cmd = media_key_cmd(code, g_last_state); pthread_mutex_unlock(&g_mu); if (cmd <= 0) return; if (cb) { cb(cmd, 0); return; } /* 本 App 是媒体源(注册过回调)→ 本地回调 */ send_playctl_upstream(cmd, 0); /* 否则经 IPC 发 launcher 转发到最近活跃会话 */ } /* 后台线程主体:懒连 + poll 读 launcher 下发 MSG_PLAYCTL → 调 cb。 */ static void *media_thread_main(void *arg) { (void)arg; while (!g_quit) { pthread_mutex_lock(&g_mu); int fd = g_fd; if (fd < 0) fd = conn_locked(); /* 懒连/重连(best-effort;失败则回到循环再试) */ pthread_mutex_unlock(&g_mu); if (fd < 0) { usleep(500000); continue; } struct pollfd p; p.fd = fd; p.events = POLLIN; p.revents = 0; int pr = poll(&p, 1, 200); if (pr < 0 && errno != EINTR) { pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); continue; } if (pr > 0 && (p.revents & (POLLHUP | POLLERR | POLLNVAL))) { pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); continue; } if (pr > 0 && (p.revents & POLLIN)) { uint8_t rbuf[MEOW_IPC_MAX]; uint16_t type; int n = read_frame(fd, &type, rbuf, sizeof rbuf); if (n < 0) { pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); continue; } if (type == MSG_PLAYCTL && n >= (int)sizeof(m_playctl_t)) { m_playctl_t c; memcpy(&c, rbuf, sizeof c); /* g_cb 经互斥读快照后调用(App 回调可能在 UI 线程,简化为直接调) */ pthread_mutex_lock(&g_mu); meow_media_playctl_fn cb = g_cb; pthread_mutex_unlock(&g_mu); if (cb) cb((int)c.cmd, (int64_t)c.seek_ms); } } } pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); return NULL; }