/* 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; } int meow_media_app_playing(const char *app_id) { const meow_media_session_t *s = meow_media_current(); if (!s || !app_id || !app_id[0]) return 0; return (s->state == MEOW_MEDIA_PLAYING && strcmp(s->app_id, app_id) == 0) ? 1 : 0; } int meow_media_app_resident(const char *app_id) { const meow_media_session_t *s = sess_find(app_id); 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; } /* 把一帧歌词分片写入会话缓冲(kind=0 lrc / kind=1 tlrc)。含分片边界截断(不越界)。 * 返回 0=写入;-1=参数非法。 */ static int lyric_put(meow_media_session_t *s, const m_lyric_t *l) { if (!s || !l) return -1; char *buf; uint32_t cap; uint32_t *lenp; if (l->kind == 0) { buf = s->lyric_lrc; cap = MEOW_MEDIA_LRC_MAX; lenp = &s->lyric_lrc_len; } else if (l->kind == 1) { buf = s->lyric_tlrc; cap = MEOW_MEDIA_TLRC_MAX; lenp = &s->lyric_tlrc_len; } else return -1; uint32_t off = l->off, n = l->len; if (off >= cap) { off = cap; n = 0; } /* 起始越界:整片丢弃 */ if (n > cap - off) n = cap - off; /* 末段越界:截断到缓冲上限 */ if (n) memcpy(buf + off, l->data, n); buf[off + n] = 0; /* 恒 NUL 结尾(供字符串读取) */ uint32_t end = off + n; if (end > *lenp) *lenp = end; return 0; } int meow_media_lyric(const m_lyric_t *l) { if (!l || !l->app_id[0]) return -1; meow_media_session_t *s = sess_find(l->app_id); if (!s) { /* 会话须先经 META 注册(否则 fd=0,PLAYCTL 会误发到 0 号描述符);丢弃。 */ media_log("media: lyric drop app=%s (no session; report META first)", l->app_id); return -1; } /* 新代际(换歌/换词):清旧词缓冲,重开分片写入。 */ if (l->seq != s->lyric_seq) { s->lyric_seq = l->seq; s->lyric_lrc_len = 0; s->lyric_tlrc_len = 0; s->lyric_lrc[0] = 0; s->lyric_tlrc[0] = 0; } lyric_put(s, l); media_log("media: LYRIC app=%s seq=%u kind=%u total=%u off=%u len=%u lrc=%u tlrc=%u", s->app_id, l->seq, l->kind, l->total, l->off, l->len, s->lyric_lrc_len, s->lyric_tlrc_len); return 0; } int meow_media_lyric_valid(const meow_media_session_t *s) { return (s && s->used && s->lyric_lrc_len > 0) ? 1 : 0; } const char *meow_media_lyric_lrc(const meow_media_session_t *s) { return (s && s->used) ? s->lyric_lrc : ""; } const char *meow_media_lyric_tlrc(const meow_media_session_t *s) { return (s && s->used) ? s->lyric_tlrc : ""; } 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\" lyric=%u", 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, (unsigned)(g_sess[i].lyric_lrc_len > 0 ? 1 : 0)); } }