/* meow_media.c — MeowOS 媒体会话注册表(阶段 D · 附录 B / D1)。 * * 实现说明见 meow_media.h。自包含:仅依赖 meow_ipc.h 结构 + (monotonic)+ * /;发送/日志经 meow_media_ops 注入,宿主可单测。 */ #include "meow_media.h" #include #include #include #include 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); } }