launcher a42171a1→ddfe616e; 宿主 test_media ALL PASS + qemu 端到端(META注册→PLAYCTL下发闭环) 出入: 卡片以媒体会话为权威非通知; PLAYCTL 走会话fd(断连防御留后续)
203 lines
6.7 KiB
C
203 lines
6.7 KiB
C
/* 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);
|
||
}
|
||
}
|