Files
AskaEth c0396e40ce sync(SDK): 增量同步主仓 M6.37–M6.39 至公开 SDK 仓
sdk/:
- 同步 meow_ui_widget/input/gfx/lazygrid/audio/media/img 等源码与头
  (SDK 默认列表/容器控件默认 MD3 — meow_list_set_container 可回退;
   全局 App 内底部上滑回桌面 — meow_ui_input_is_home_swipe 系统保留手势;
   LazyGrid/LazyColumn/LazyRow MD3 化;叠加让位自抑改 app_id 判定;
   DSD 音频格式等)
- 新增独立模块源码 meow_img_color / meow_lyric / meow_lyric_view 及对应宿主 test_*.c
- 保留公开仓自包含适配:vendored meow_font.c/meow_font.h/meow_notif.h 不动;
  build_sdk.sh 自包含版本不动;meow_ui_gfx.c 仅改 meow_font.h include 路径;
  meow_ui_internal.h 去除 FreeType 内部声明
- meow_ipc.h 同步到主仓 launcher 版本(含 AUDIO_FMT_DOP/媒体歌词/蓝牙输出协议)
- 不含 FreeType 矢量字 meow_ft_*(需 freetype vendor,公开仓以位图 meow_font
  自包含);不含 mipsel 二进制 / .o / .a / 固件 / 密钥 / devdocs

examples/hello/:
- 新增 ui/demo/sdk2/sdk2.c;build_hello.sh 纳入该文件(保持自包含)

docs / README / .gitignore:
- docs/MEOW_APP_SDK.md 补 §12.5(默认 MD3 容器 + 底部上滑回桌面 + 两个新 API)
- README 能力清单补「默认 MD3 容器」与「底部上滑回桌面」
- .gitignore 补新增宿主测试二进制

验证:sdk/meow_ui.a 与 examples/hello/hello 交叉编译均 0 warning;工作区无生成物。
2026-09-04 18:05:36 +08:00

314 lines
12 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.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
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;
/* 后台线程主体(定义在文件后段;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();
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 6=播放/暂停切换)。
* 播放/暂停(164)恒发 cmd=6 toggle——SDK 本地不再按「最近上报 state」判 play/pause:
* 走自有 IPC 线程的媒体源(如 ncm 的 ipc_media.c)不经 meow_ui_media_meta 上报,
* 本地 state 恒 0 → 恒发「播放」,播放中按键无效(真机 bug)。toggle 由媒体源按自身
* 当前态翻转(权威在源)。上一曲 165/19→3,下一曲 163→4。非媒体键返回 -1。 */
static int media_key_cmd(int code)
{
switch (code) {
case 164: /* KEY_PLAYPAUSE:toggle(源侧按当前态翻转) */
return 6;
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;
pthread_mutex_unlock(&g_mu);
int cmd = media_key_cmd(code);
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;
}