/* play/ipc_media.c — 媒体会话 IPC 最小客户端(SDK 尚无媒体封装,App 内用 meow_ipc.h 帧格式直连)。 * * 连接:AF_UNIX SOCK_STREAM 到 MEOW_IPC_SOCK(缺省 /usr/data/meow_ipc.sock)。HELLO(app_id, * MF_PLAYBACK) 注册为媒体源;之后**同一连接**周期发 MSG_META(~1s,进度随播放推进)并阻塞等 * launcher 下发 MSG_PLAYCTL(播放/暂停/上下曲/seek/toggle)。launcher 经该连接 fd 回发(meow_media.c * send_playctl 用最近活跃会话的 fd)。无 socket/launcher 不跑 → 优雅跳过(打日志不崩)。 * 线程模型:独立线程(main 起了 meow_ui_run 阻塞;播放线程归 player_manager,本线程只做 IPC 收发)。 */ #include "../ncm_app_internal.h" #include #include #include #include #include #include #include #include #include #include "meow_ipc.h" static pthread_t g_thr; static int g_thr_on; static int g_quit; 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; } /* 发送一帧(头+负载)。 */ 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); size_t off = 0; while (off < sizeof hdr) { ssize_t w = write(fd, hdr + off, sizeof hdr - off); if (w <= 0) return -1; off += (size_t)w; } if (len && payload) { off = 0; while (off < len) { ssize_t w = write(fd, (const uint8_t *)payload + off, len - off); if (w <= 0) return -1; off += (size_t)w; } } return 0; } static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap) { uint8_t hdr[MEOW_IPC_HDR_LEN]; size_t off = 0; while (off < sizeof hdr) { ssize_t r = read(fd, hdr + off, sizeof hdr - off); if (r <= 0) return -1; off += (size_t)r; } 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; 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; } /* 组并发送一条 META。 */ static void send_meta(int fd, uint32_t req_id) { m_meta_t mm; memset(&mm, 0, sizeof mm); ncm_strlcpy(mm.app_id, "dev.player.ncm", sizeof mm.app_id); int idx = player_manager_playing_song(); pthread_mutex_lock(&g_play.mu); int pos = (int)g_play.pos_ms; int dur = (int)g_play.dur_ms; int state = g_play.state; int err = g_play.err; pthread_mutex_unlock(&g_play.mu); static char title[128], artist[128], album[128]; title[0] = artist[0] = album[0] = 0; ncm_song_t sm; if (idx >= 0 && player_manager_current_song(&sm) == 0) { /* 播放快照(窗口无关,§17) */ snprintf(title, sizeof title, "%s", sm.name); snprintf(artist, sizeof artist, "%s", sm.art_name); snprintf(album, sizeof album, "%s", sm.album_name); } snprintf(mm.title, sizeof mm.title, "%s", title); snprintf(mm.artist, sizeof mm.artist, "%s", artist); snprintf(mm.album, sizeof mm.album, "%s", album); mm.pos_ms = (uint32_t)(pos < 0 ? 0 : pos); mm.dur_ms = (uint32_t)(dur < 0 ? 0 : dur); uint32_t st = 0; if (err != 0) st = 0; else if (state == NCM_PC_PLAYING) st = 1; else if (state == NCM_PC_PAUSED) st = 2; else if (state == NCM_PC_BUFFER) st = 3; else st = 0; mm.state = st; /* 封面:若已落盘(cover 缓存文件)则提供路径 */ mm.flags = 0; const char *cp = cover_current_path(); if (cp && cp[0]) { snprintf(mm.artwork, sizeof mm.artwork, "%s", cp); mm.flags |= MF_ART | MF_COVER; } if (lyric_count() > 0) mm.flags |= MF_LYRIC; send_frame(fd, MSG_META, req_id, &mm, sizeof mm); } static void apply_playctl(const m_playctl_t *c) { switch (c->cmd) { case NCM_CTL_PLAY: player_manager_pause(0); break; case NCM_CTL_PAUSE: player_manager_pause(1); break; case NCM_CTL_PREV: player_manager_next(-1); break; case NCM_CTL_NEXT: player_manager_next(1); break; case NCM_CTL_SEEK: player_manager_seek(c->seek_ms); break; case NCM_CTL_TOGGLE: /* 物理播放/暂停键(SDK 164 恒发 toggle,不再本地判 state):源是状态权威, * 按当前态翻转(与播放页按钮 ui/player.c 同款判定)。非 PLAYING/PAUSED * (停止/缓冲)时 pause() 内部按态过滤,天然 no-op 安全。 */ player_manager_pause(player_manager_state() == NCM_PC_PAUSED ? 0 : 1); break; default: break; } } static void *ipc_thread(void *arg) { (void)arg; int fd = connect_path(sock_path()); if (fd < 0) { meow_log(MEOW_LOG_WARN, "ipc: no launcher socket"); return NULL; } m_hello_t h; memset(&h, 0, sizeof h); ncm_strlcpy(h.app_id, "dev.player.ncm", sizeof h.app_id); h.version = 1; h.flags = MF_PLAYBACK; if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) != 0) { close(fd); return NULL; } uint8_t hb[256]; uint16_t htype; if (read_frame(fd, &htype, hb, sizeof hb) < 0) { close(fd); return NULL; } send_meta(fd, 2); /* 首条注册媒体源(state=0 未播也登记,start 播后自动成源) */ uint64_t last_meta = 0; while (!g_quit && !g_app_quit) { struct pollfd p; p.fd = fd; p.events = POLLIN; int pr = poll(&p, 1, 200); 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) break; if (type == MSG_PLAYCTL && n >= (int)sizeof(m_playctl_t)) { m_playctl_t c; memcpy(&c, rbuf, sizeof c); apply_playctl(&c); } } else if (pr < 0 && errno != EINTR) { break; } uint64_t now = meow_ui_time_ms(); if (now - last_meta >= 1000) { send_meta(fd, (uint32_t)(now & 0x7FFFFFFF)); last_meta = now; } } close(fd); return NULL; } int ipc_media_start(void) { if (g_thr_on) return 0; g_quit = 0; if (pthread_create(&g_thr, NULL, ipc_thread, NULL) != 0) return -1; pthread_detach(g_thr); g_thr_on = 1; return 0; } void ipc_media_stop(void) { g_quit = 1; /* 线程在 200ms poll 内退出(detach,不 join)*/ g_thr_on = 0; }