feat(launcher): 媒体会话+Now Playing (agent-204) — MSG_META(0x72)/MSG_PLAYCTL(0x73)+meow_media.c(最近活跃源=active_seq净序,30s超时回收); 通知中心顶部媒体卡片(封面缓存+进度seek+控制键); 主界面中部/右缘左滑进 Now Playing 大页(封面+元数据+进度+控制+歌词占位), 左缘返回; 空态完备

launcher a42171a1→ddfe616e; 宿主 test_media ALL PASS + qemu 端到端(META注册→PLAYCTL下发闭环)
出入: 卡片以媒体会话为权威非通知; PLAYCTL 走会话fd(断连防御留后续)
This commit is contained in:
MeowOS committed 2026-08-31 06:51:04 +08:00
1 parent 8012948b26
commit 32543ac616
15 files changed
+1429 -34

No files matched your search

+202
View File
@@ -0,0 +1,202 @@
/* meow_media.c — MeowOS 媒体会话注册表(阶段 D · 附录 B / D1)。
*
* 实现说明见 meow_media.h。自包含:仅依赖 meow_ipc.h 结构 + <time.h>(monotonic)+
* <string.h>/<stdarg.h>;发送/日志经 meow_media_ops 注入,宿主可单测。
*/
#include "meow_media.h"
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
static const meow_media_ops *g_ops = NULL;
static meow_media_session_t g_sess[MEOW_MEDIA_MAX];
static uint64_t g_seq = 0; /* 全局活跃净序(每次 META +1,严格区分先后) */
/* 单调毫秒(self-contained;launcher/main_menu 的 now_ms 与此同源 CLOCK_MONOTONIC)。 */
static uint64_t media_now_ms(void)
{
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) ts.tv_sec = ts.tv_nsec = 0;
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000u);
}
static void media_log(const char *fmt, ...)
{
if (!g_ops || !g_ops->log) return;
char buf[256];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
g_ops->log("%s", buf);
}
static char *media_cp(char *dst, size_t dstsz, const char *src)
{
if (!dst || dstsz == 0) return dst;
if (!src) src = "";
snprintf(dst, dstsz, "%s", src);
dst[dstsz - 1] = 0;
return dst;
}
void meow_media_init(const meow_media_ops *ops)
{
g_ops = ops;
}
static meow_media_session_t *sess_find(const char *app_id)
{
if (!app_id || !app_id[0]) return NULL;
for (int i = 0; i < MEOW_MEDIA_MAX; i++)
if (g_sess[i].used && strcmp(g_sess[i].app_id, app_id) == 0)
return &g_sess[i];
return NULL;
}
/* 找空槽;满时覆盖最旧槽(LRU 式,防单源漂移占满)。 */
static meow_media_session_t *sess_alloc(void)
{
int slot = -1;
uint64_t oldest = UINT64_MAX;
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) return &g_sess[i];
if (g_sess[i].last_active_ms < oldest) { oldest = g_sess[i].last_active_ms; slot = i; }
}
if (slot >= 0) {
media_log("media: sessions full, evict app=%s", g_sess[slot].app_id);
memset(&g_sess[slot], 0, sizeof g_sess[slot]);
return &g_sess[slot];
}
return NULL;
}
int meow_media_meta(const m_meta_t *m, int fd)
{
if (!m || !m->app_id[0]) return -1;
meow_media_session_t *s = sess_find(m->app_id);
if (!s) {
s = sess_alloc();
if (!s) { media_log("media: meta drop app=%s (no slot)", m->app_id); return -1; }
s->used = 1;
media_cp(s->app_id, sizeof s->app_id, m->app_id);
}
s->fd = fd;
s->last_active_ms = media_now_ms();
s->active_seq = ++g_seq; /* 严格递增:跨会话先后唯一,不受时钟粒度影响 */
s->state = m->state;
s->pos_ms = m->pos_ms;
s->dur_ms = m->dur_ms;
s->flags = m->flags;
media_cp(s->title, sizeof s->title, m->title);
media_cp(s->artist, sizeof s->artist, m->artist);
media_cp(s->album, sizeof s->album, m->album);
media_cp(s->artwork, sizeof s->artwork, m->artwork);
media_cp(s->lyric, sizeof s->lyric, m->lyric);
media_log("media: META app=%s fd=%d state=%u pos=%u/%u title=\"%s\"",
s->app_id, fd, s->state, s->pos_ms, s->dur_ms, s->title);
return 0;
}
const meow_media_session_t *meow_media_current(void)
{
/* 最近活跃 = active_seq 最大者(严格先后;last_active_ms 仅作进度推进用)。 */
const meow_media_session_t *best = NULL;
uint64_t t = 0;
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (g_sess[i].used && g_sess[i].active_seq >= t) {
t = g_sess[i].active_seq;
best = &g_sess[i];
}
}
return best;
}
int meow_media_has_source(void)
{
const meow_media_session_t *s = meow_media_current();
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
}
static int send_playctl(const meow_media_session_t *s, uint32_t cmd, int64_t seek_ms)
{
if (!s || !s->used || !g_ops || !g_ops->send) return -1;
m_playctl_t c;
memset(&c, 0, sizeof c);
c.cmd = cmd;
c.seek_ms = seek_ms;
size_t n = g_ops->send(s->fd, MSG_PLAYCTL, 0, &c, sizeof c);
media_log("media: PLAYCTL -> %s cmd=%u seek=%lld n=%zu",
s->app_id, cmd, (long long)seek_ms, n);
/* ops->send 返回写入总字节(16B 头 + 负载);成功 = 完整帧已写出。 */
return (n >= MEOW_IPC_HDR_LEN + sizeof c) ? 0 : -1;
}
int meow_media_playctl(uint32_t cmd, int64_t seek_ms)
{
return send_playctl(meow_media_current(), cmd, seek_ms);
}
int meow_media_playctl_to(const char *app_id, uint32_t cmd, int64_t seek_ms)
{
meow_media_session_t *s = sess_find(app_id);
return s ? send_playctl(s, cmd, seek_ms) : -1;
}
void meow_media_clear(const char *app_id)
{
int n = 0;
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (g_sess[i].used &&
(!app_id || !app_id[0] || strcmp(g_sess[i].app_id, app_id) == 0)) {
media_log("media: clear app=%s", g_sess[i].app_id);
memset(&g_sess[i], 0, sizeof g_sess[i]);
n++;
}
}
if (app_id && app_id[0]) media_log("media: cleared %d sess for %s", n, app_id);
else if (n) media_log("media: cleared all %d sess", n);
}
void meow_media_tick(void)
{
uint64_t now = media_now_ms();
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) continue;
if (g_sess[i].state == MEOW_MEDIA_PLAYING || g_sess[i].state == MEOW_MEDIA_BUFFER) {
if (now - g_sess[i].last_active_ms > MEOW_MEDIA_STALE_MS) {
media_log("media: stale source %s (no META %llu ms) -> clear",
g_sess[i].app_id,
(unsigned long long)(now - g_sess[i].last_active_ms));
memset(&g_sess[i], 0, sizeof g_sess[i]);
}
}
}
}
uint32_t meow_media_display_pos(const meow_media_session_t *s)
{
if (!s) return 0;
uint32_t p = s->pos_ms;
if (s->state == MEOW_MEDIA_PLAYING) {
uint64_t now = media_now_ms();
uint64_t el = (now > s->last_active_ms) ? (now - s->last_active_ms) : 0;
uint32_t adv = (el > 0xFFFFFFFFull) ? 0xFFFFFFFFu : (uint32_t)el;
p = s->pos_ms + adv;
if (s->dur_ms && p > s->dur_ms) p = s->dur_ms;
}
return p;
}
void meow_media_dump(const char *prefix)
{
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
if (!g_sess[i].used) continue;
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\"",
prefix ? prefix : "media", i, g_sess[i].app_id, g_sess[i].state,
g_sess[i].fd, (unsigned long long)g_sess[i].last_active_ms,
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title);
}
}