Files

342 lines
13 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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);
}