342 lines
13 KiB
C
342 lines
13 KiB
C
/* 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <poll.h>
|
||
#include <sys/socket.h>
|
||
#include <sys/un.h>
|
||
#include "meow_ipc.h"
|
||
|
||
static pthread_t g_thr;
|
||
static int g_thr_on;
|
||
static int g_quit;
|
||
static volatile int g_req_focus_player = 0; /* 任务3:launcher 请求「回到播放页」(UI 线程消费) */
|
||
|
||
static const char *sock_path(void)
|
||
{
|
||
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;
|
||
case NCM_CTL_FOCUS:
|
||
/* 任务3:通知中心媒体卡片点「卡片体」→ launcher 发本 cmd(需要 launcher 接线,见任务书):
|
||
* 请求 App 回到播放页。只置标志(跨线程),UI 线程 app_on_tick 消费后导航(page nav 非线程安全)。 */
|
||
g_req_focus_player = 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;
|
||
}
|
||
|
||
/* 任务2:home 根页左滑「回桌面后台」——不再进程退出,改发 MSG_NAV{EXIT} 给 launcher。
|
||
* launcher M4 据 app.json background_audio + 媒体会话 PLAYING(meow_media_app_playing)判
|
||
* 保活(进程+音频流保留转后台,UI 走 §10.5 SDK present 自抑,非 SIGSTOP);否则才真终止。
|
||
* 本函数**不**置 g_app_quit、**不**停线程——进程保留,媒体会话继续上报 PLAYING(任务2 ③)。
|
||
* 独立连接发一帧(launcher ipc_dispatch MSG_NAV{EXIT} → meow_nav_request_exit,app_launcher.c:571)。
|
||
* 返回 0=发出 / -1=无 socket 或写失败。 */
|
||
int ipc_media_nav_exit(void)
|
||
{
|
||
int fd = connect_path(sock_path());
|
||
if (fd < 0) {
|
||
meow_log(MEOW_LOG_WARN, "ipc: nav_exit no launcher socket");
|
||
return -1;
|
||
}
|
||
m_nav_t n;
|
||
memset(&n, 0, sizeof n);
|
||
ncm_strlcpy(n.app_id, "dev.player.ncm", sizeof n.app_id);
|
||
n.nav_type = MEOW_NAV_EXIT;
|
||
n.depth = 0;
|
||
int rc = send_frame(fd, MSG_NAV, 9, &n, sizeof n);
|
||
close(fd);
|
||
meow_log(MEOW_LOG_INFO, "ipc: nav_exit sent rc=%d (request launcher background)", rc);
|
||
return rc;
|
||
}
|
||
|
||
/* 任务4 keep_awake:ACT_* 值 → launcher app.json awake/route 键用的活动名。
|
||
* 命中的只有这些(ncm_app_internal.h ACT_* 枚举);未知/空栈(0) 返回 NULL(不起作用)。 */
|
||
static const char *ncm_act_name(int act)
|
||
{
|
||
switch (act) {
|
||
case ACT_LOGIN: return "login";
|
||
case ACT_HOME: return "home";
|
||
case ACT_PLAYLIST: return "playlist";
|
||
case ACT_PLAYER: return "player";
|
||
case ACT_SETTINGS: return "settings";
|
||
case ACT_SEARCH: return "search";
|
||
default: return NULL;
|
||
}
|
||
}
|
||
|
||
/* 任务4 keep_awake:把当前顶层 activity 上报 launcher(MSG_ACT_FOCUS,独立连接发一帧,无需 HELLO)。
|
||
* launcher util.c ipc_dispatch MSG_ACT_FOCUS → meow_fg_act_set(app,act);fg_keep_awake() 依
|
||
* app.json awake.<act>=1 判定该 activity 是否常亮(超时不自动息屏)。无 socket/launcher 不跑 → 优雅跳过。
|
||
* 返回 0=发出 / -1=无 socket 或写失败。 */
|
||
int ipc_act_report(int act)
|
||
{
|
||
const char *name = ncm_act_name(act);
|
||
if (!name) return -1;
|
||
int fd = connect_path(sock_path());
|
||
if (fd < 0) {
|
||
meow_log(MEOW_LOG_WARN, "ipc: act_focus(%s) no launcher socket", name);
|
||
return -1;
|
||
}
|
||
m_act_focus_t f;
|
||
memset(&f, 0, sizeof f);
|
||
ncm_strlcpy(f.app_id, "dev.player.ncm", sizeof f.app_id);
|
||
ncm_strlcpy(f.activity, name, sizeof f.activity);
|
||
int rc = send_frame(fd, MSG_ACT_FOCUS, 10, &f, sizeof f);
|
||
close(fd);
|
||
meow_log(MEOW_LOG_INFO, "ipc: act_focus act=%s rc=%d", name, rc);
|
||
return rc;
|
||
}
|
||
|
||
/* 任务3:UI 线程消费「回到播放页」标志。返回 1=有未消费的聚焦请求(消费一次)。 */
|
||
int ipc_media_focus_consume(void)
|
||
{
|
||
if (g_req_focus_player) {
|
||
g_req_focus_player = 0;
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int ipc_media_start(void)
|
||
{
|
||
if (g_thr_on) return 0;
|
||
g_quit = 0;
|
||
if (ncm_thread_create(&g_thr, 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;
|
||
}
|
||
|
||
/* ---- 批3:live 歌词上报(lrc 原文 + tlrc 翻译 → 媒体会话,桌面歌词页实时滚动源)---- */
|
||
|
||
/* 歌词代际(换歌/换词自增;launcher 见新 seq 清旧词)。IC 线程外调用(lyric_worker),
|
||
* 单线程写(worker 是唯一调用者),无需锁。 */
|
||
static uint32_t g_lyr_media_seq;
|
||
|
||
/* 把一个 kind 的歌词文本按分片发到媒体会话(≤MEOW_LYRIC_CHUNK 一帧)。cmax=该 kind 上限:
|
||
* 0=MEOW_MEDIA_LRC_MAX,1=MEOW_MEDIA_TLRC_MAX。total=0(空串)时发一个空帧=清词。 */
|
||
static void send_lyric_kind(int fd, uint32_t seq, uint16_t kind, const char *text, size_t cmax)
|
||
{
|
||
if (!text) text = "";
|
||
size_t total = strlen(text);
|
||
if (total > cmax) total = cmax; /* 接收侧缓冲上限:超限截断 */
|
||
size_t off = 0;
|
||
do {
|
||
size_t n = total - off;
|
||
if (n > MEOW_LYRIC_CHUNK) n = MEOW_LYRIC_CHUNK;
|
||
m_lyric_t ly;
|
||
memset(&ly, 0, sizeof ly);
|
||
ncm_strlcpy(ly.app_id, "dev.player.ncm", sizeof ly.app_id);
|
||
ly.seq = seq;
|
||
ly.kind = kind;
|
||
ly.total = (uint16_t)total;
|
||
ly.off = (uint16_t)off;
|
||
ly.len = (uint16_t)n;
|
||
if (n) memcpy(ly.data, text + off, n);
|
||
ly.data[n] = 0;
|
||
send_frame(fd, MSG_META_LYRIC, seq, &ly, sizeof ly);
|
||
off += n;
|
||
} while (off < total);
|
||
}
|
||
|
||
void ipc_media_lyric_report(const char *lrc, const char *tl)
|
||
{
|
||
g_lyr_media_seq++;
|
||
int fd = connect_path(sock_path());
|
||
if (fd < 0) { meow_log(MEOW_LOG_WARN, "ipc: lyric no launcher socket"); return; }
|
||
send_lyric_kind(fd, g_lyr_media_seq, 0, lrc, MEOW_MEDIA_LRC_MAX); /* 原文 */
|
||
send_lyric_kind(fd, g_lyr_media_seq, 1, tl, MEOW_MEDIA_TLRC_MAX); /* 翻译 */
|
||
close(fd);
|
||
meow_log(MEOW_LOG_INFO, "ipc: lyric reported seq=%u lrc=%zuB tlrc=%zuB",
|
||
g_lyr_media_seq, lrc ? strlen(lrc) : 0, tl ? strlen(tl) : 0);
|
||
}
|