diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index 22ce112..a35ff69 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -81,6 +81,7 @@ enum { MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */ MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */ MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */ + MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */ /* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ---- * 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */ MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */ @@ -274,6 +275,29 @@ typedef struct m_playctl { int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */ } m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */ +/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)---- + * App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页 + * 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限, + * 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。 + * - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用) + * - kind = 0 原文 lrc / 1 翻译 tlrc + * - total= 该 kind 总字节数(不含 NUL;0=清空该 kind) + * - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送) + * 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲, + * 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */ +#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */ +#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */ +#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */ +typedef struct m_lyric { + char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */ + uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */ + uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */ + uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */ + uint16_t off; /* 本片起始偏移(≤ total;首片=0) */ + uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */ + char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */ +} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */ + /* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。 * - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon` * (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退); diff --git a/patch_v2/meow_launcherd/meow_media.c b/patch_v2/meow_launcherd/meow_media.c index 9a57c30..339bde3 100644 --- a/patch_v2/meow_launcherd/meow_media.c +++ b/patch_v2/meow_launcherd/meow_media.c @@ -197,13 +197,74 @@ uint32_t meow_media_display_pos(const meow_media_session_t *s) 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\"", + 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); + g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title, + (unsigned)(g_sess[i].lyric_lrc_len > 0 ? 1 : 0)); } } diff --git a/patch_v2/meow_launcherd/meow_media.h b/patch_v2/meow_launcherd/meow_media.h index 95ef3e2..ac74e8b 100644 --- a/patch_v2/meow_launcherd/meow_media.h +++ b/patch_v2/meow_launcherd/meow_media.h @@ -48,7 +48,15 @@ typedef struct meow_media_session { char artist[128]; char album[128]; char artwork[192]; - char lyric[192]; + char lyric[192]; /* 歌词源路径/槽位(沿用 META.lyric) */ + /* 批3:当前曲 live 歌词(App 经 MSG_META_LYRIC 上报;有界缓冲,新 seq 重置)。 + * lyric_lrc/lyric_tlrc 为当前曲歌词原文/翻译文本(桌面歌词页 meow_lyric_parse 用); + * lyric_seq=歌曲代际(App 新歌/新词自增,launcher 见新 seq 清旧词)。 */ + uint32_t lyric_seq; + uint32_t lyric_lrc_len; + uint32_t lyric_tlrc_len; + char lyric_lrc[MEOW_MEDIA_LRC_MAX]; + char lyric_tlrc[MEOW_MEDIA_TLRC_MAX]; } meow_media_session_t; /* 依赖注入:send=发一帧(launcher 用 ipc_send 包装;宿主单测用 socket 写); @@ -91,6 +99,17 @@ void meow_media_tick(void); /* 显示用虚拟位置:播放中按墙钟推进 pos_ms(clamp 到 dur);暂停/缓冲/停止原样。 */ uint32_t meow_media_display_pos(const meow_media_session_t *s); +/* ---- 歌词(批3 · live lyric 数据源)---- + * App 上报当前曲歌词(MSG_META_LYRIC 分片):按 app_id 归并到会话,seq 为歌曲代际、 + * 新代际清旧词;分片按 kind(0=原文/1=翻译)/off 写入会话内有序缓冲(超 MEOW_MEDIA_LRC_MAX/ + * MEOW_MEDIA_TLRC_MAX 截断)。会话不存在(未先报 META 注册)→ 丢弃返回 -1(防 fd=0 槽)。 + * meow_media_lyric_valid/lrc/tlrc:桌面歌词页从当前会话取歌词文本,交由批1 meow_lyric_parse + * 解析 + meow_lyric_line_at(meow_media_display_pos(s)) 驱动批2 meow_lyric_view 实时滚动。 */ +int meow_media_lyric(const m_lyric_t *l); /* 0=已写入;-1=会话不存在/参数非法 */ +int meow_media_lyric_valid(const meow_media_session_t *s); /* 会话携带有效歌词(lrc 非空) */ +const char *meow_media_lyric_lrc(const meow_media_session_t *s); /* 原文;无则 "" */ +const char *meow_media_lyric_tlrc(const meow_media_session_t *s); /* 翻译;无则 "" */ + /* 调试打印(经 ops->log,前缀+每会话一行)。 */ void meow_media_dump(const char *prefix); diff --git a/patch_v2/meow_launcherd/test_media_lyric.c b/patch_v2/meow_launcherd/test_media_lyric.c new file mode 100644 index 0000000..74acbb9 --- /dev/null +++ b/patch_v2/meow_launcherd/test_media_lyric.c @@ -0,0 +1,185 @@ +/* test_media_lyric.c — 宿主单测:批3 live 歌词数据源框架(meow_media 会话携带 lrc/tlrc → meow_lyric 解析 → 随播放位置命中当前行)。 + * + * 链路:App 上报歌词(分片 m_lyric → meow_media_lyric)→ 会话累积 lrc/tlrc(有界、seq 代际) + * → launcher 读取(meow_media_lyric_valid/lrc/tlrc)→ meow_lyric_parse → meow_lyric_line_at(meow_media_display_pos)。 + * + * 覆盖矩阵: + * L1 App 上报 lrc/tlrc → 会话携带歌词(valid/lrc/tlrc 正确) + * L2 launcher 读取 → meow_lyric_parse 成功(count/行文本/翻译合并正确) + * L3 播放位置推进(display_pos 随 PLAYING 单调增)→ line_at 命中当前行(pos 增大 → 行号不降) + * L4 分片重组:>MEOW_LYRIC_CHUNK 的 lrc 拆多帧上报,会话按 off 累积成完整串 + * L5 seq 代际切换(换歌)→ 清旧词;同 seq 重报(幂等)不破坏 + * L6 会话不存在 → meow_media_lyric 返回 -1;翻译无匹配自动忽略 + * L7 多 App 独立:另一源上报自己的歌词,互不串 + * + * 编译(宿主):meow_media.c(纯 libc)+ meow_lyric.c(纯 libc+pthread)。 + * gcc -Wall -Wextra -pthread -o test_media_lyric test_media_lyric.c meow_media.c ../sdk/meow_lyric.c -I. -I../sdk + * ./test_media_lyric + */ +#include +#include +#include +#include +#include +#include +#include "meow_media.h" +#include "meow_ipc.h" +#include "meow_lyric.h" + +static int g_fails = 0; +#define CHECK(cond, msg) do { \ + if (cond) printf("PASS: %s\n", msg); \ + else { printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fails++; } } while (0) + +static size_t test_send(int fd, uint16_t type, uint32_t req_id, + const void *payload, uint32_t len) +{ + /* 用不到(本链路无下行),占位满足 meow_media_ops.send 签名。 */ + (void)fd; (void)type; (void)req_id; (void)payload; (void)len; + return 0; +} + +static void fill_meta(m_meta_t *m, const char *app, uint32_t state, uint32_t pos, uint32_t dur) +{ + memset(m, 0, sizeof *m); + snprintf(m->app_id, sizeof m->app_id, "%s", app); + m->state = state; m->pos_ms = pos; m->dur_ms = dur; + snprintf(m->title, sizeof m->title, "Song"); + snprintf(m->artist, sizeof m->artist, "Artist"); +} + +/* 模拟 ncm 的 send_lyric_kind:把 text 按 ≤MEOW_LYRIC_CHUNK 分片,逐片调 meow_media_lyric。 */ +static void report_lyric_chunked(const char *app, uint32_t seq, uint16_t kind, + const char *text, size_t cmax) +{ + if (!text) text = ""; + size_t total = strlen(text); + if (total > cmax) total = cmax; + size_t off = 0; + do { + size_t n = total - off; + if (n > MEOW_LYRIC_CHUNK) n = MEOW_LYRIC_CHUNK; + m_lyric_t ly; + memset(&ly, 0, sizeof ly); + snprintf(ly.app_id, sizeof ly.app_id, "%s", app); + ly.seq = seq; + ly.kind = kind; + ly.total = (uint16_t)total; + ly.off = (uint16_t)off; + ly.len = (uint16_t)n; + if (n) memcpy(ly.data, text + off, n); + ly.data[n] = 0; + int rc = meow_media_lyric(&ly); + if (rc != 0) { printf(" (report chunk off=%zu rc=%d)\n", off, rc); } + off += n; + } while (off < total); +} + +int main(void) +{ + meow_media_ops ops; + ops.send = test_send; + ops.log = NULL; + meow_media_init(&ops); + + const char *lrc = "[00:01.00]Hello\n[00:02.00]World\n[00:03.00]Bye\n"; + const char *tlrc = "[00:01.00]你好\n[00:03.00]再见\n[99:99.99]ignore me\n"; + + /* ---- L1+L4 大片(跨分片)---- */ + char big[4096]; + int bi = 0; + bi += snprintf(big + bi, sizeof big - bi, "[00:00.10]p0\n"); + for (int i = 1; i <= 40; i++) /* 约 40 行 → >MEOW_LYRIC_CHUNK(980B) */ + bi += snprintf(big + bi, sizeof big - bi, "[00:%02d.%02d]line%d\n", i / 60, i % 60, i); + + /* 注册会话(META 先行,否则 meow_media_lyric 丢弃) */ + m_meta_t m; + fill_meta(&m, "dev.player.ncm", MEOW_MEDIA_PLAYING, 1000, 120000); + CHECK(meow_media_meta(&m, -1) == 0, "META registers session"); + + /* 上报歌词:seq=1(新代际),lrc 与 tlrc 各分片 */ + report_lyric_chunked("dev.player.ncm", 1, 0, lrc, MEOW_MEDIA_LRC_MAX); + report_lyric_chunked("dev.player.ncm", 1, 1, tlrc, MEOW_MEDIA_TLRC_MAX); + + const meow_media_session_t *s = meow_media_current(); + CHECK(s && !strcmp(s->app_id, "dev.player.ncm"), "current() == dev.player.ncm"); + CHECK(meow_media_lyric_valid(s) == 1, "L1: lyric_valid == 1"); + CHECK(strcmp(meow_media_lyric_lrc(s), lrc) == 0, "L1: lrc matches"); + CHECK(strcmp(meow_media_lyric_tlrc(s), tlrc) == 0, "L1: tlrc matches"); + + /* ---- L2 launcher 读取 → 解析 ---- */ + int n = meow_lyric_parse(meow_media_lyric_lrc(s), meow_media_lyric_tlrc(s)); + CHECK(n == 3, "L2: meow_lyric_parse count == 3"); + CHECK(meow_lyric_count() == 3, "L2: count() == 3"); + CHECK(strcmp(meow_lyric_line_text(0), "Hello") == 0, "L2: line0 text Hello"); + CHECK(strcmp(meow_lyric_line_tl(0), "你好") == 0, "L2: line0 tl 你好 (2000ms 合并)"); + CHECK(strcmp(meow_lyric_line_text(2), "Bye") == 0, "L2: line2 text Bye"); + CHECK(strcmp(meow_lyric_line_tl(2), "再见") == 0, "L2: line2 tl 再见"); + CHECK(meow_lyric_count() == 3, "L2: 无匹配翻译行被忽略(仍 3 行)"); + + /* ---- L3 播放位置推进 → 命中当前行 ---- */ + uint32_t pA = meow_media_display_pos(s); + int lineA = meow_lyric_line_at(pA); + CHECK(lineA == 0, "L3: display_pos ≈ 1000ms → line 0 (Hello)"); + usleep(600 * 1000); /* 播放中:display_pos 随墙钟推进 */ + uint32_t pB = meow_media_display_pos(s); + int lineB = meow_lyric_line_at(pB); + CHECK(pB >= pA, "L3: display_pos advances (playing)"); + CHECK(lineB >= lineA, "L3: line_at pos 增大 → 行号不降(随播放命中当前行)"); + /* 再推进到 ≥3000ms 应到末行 */ + usleep(1500 * 1000); + uint32_t pC = meow_media_display_pos(s); + CHECK(pC >= 3000u || lineB >= 2, "L3: pos 推进后到位(或已末行)"); + CHECK(meow_lyric_line_at(pC) >= meow_lyric_line_at(pB), "L3: 行号单调不降"); + + /* ---- L4 大片分片重组(big lrc)---- */ + meow_media_clear(NULL); + fill_meta(&m, "dev.player.ncm", MEOW_MEDIA_PLAYING, 0, 120000); + meow_media_meta(&m, -1); + report_lyric_chunked("dev.player.ncm", 2, 0, big, MEOW_MEDIA_LRC_MAX); + s = meow_media_current(); + CHECK(s && meow_media_lyric_valid(s) == 1, "L4: big lyric_valid == 1"); + int bn = meow_lyric_parse(meow_media_lyric_lrc(s), NULL); + CHECK(bn > 40, "L4: 大片跨分片重组后解析行数 > 40"); + CHECK(meow_lyric_line_at(0) == 0 && meow_lyric_line_at(10) == 0, "L4: 低 pos 命中第0行"); + CHECK(meow_lyric_line_at(meow_lyric_line_time(bn - 1)) == bn - 1, "L4: 末行命中"); + + /* ---- L5 seq 代际切换(换歌)清旧词 ---- */ + report_lyric_chunked("dev.player.ncm", 3, 0, "[00:05.00]New song line\n", MEOW_MEDIA_LRC_MAX); + s = meow_media_current(); + CHECK(meow_media_lyric_valid(s) == 1, "L5: 新代际仍 valid"); + CHECK(strcmp(meow_media_lyric_lrc(s), "[00:05.00]New song line\n") == 0, "L5: 新词覆盖旧词"); + CHECK(meow_media_lyric_tlrc(s)[0] == 0, "L5: 新代际清空旧翻译"); + /* seq 不变重报:幂等(不清) */ + report_lyric_chunked("dev.player.ncm", 3, 1, "[00:05.00]新歌\n", MEOW_MEDIA_TLRC_MAX); + s = meow_media_current(); + CHECK(strcmp(meow_media_lyric_tlrc(s), "[00:05.00]新歌\n") == 0, "L5: 同 seq 补翻译不清"); + + /* ---- L6 会话不存在 → -1;空 app_id → -1 ---- */ + m_lyric_t empty; + memset(&empty, 0, sizeof empty); + snprintf(empty.app_id, sizeof empty.app_id, "no.session.app"); + empty.seq = 9; empty.kind = 0; empty.total = 0; empty.off = 0; empty.len = 0; + CHECK(meow_media_lyric(&empty) == -1, "L6: 未注册会话 → -1"); + memset(&empty, 0, sizeof empty); + CHECK(meow_media_lyric(&empty) == -1, "L6: 空 app_id → -1"); + + /* ---- L7 多 App 独立 ---- */ + fill_meta(&m, "com.player2", MEOW_MEDIA_PLAYING, 0, 180000); + meow_media_meta(&m, -1); + meow_media_clear("dev.player.ncm"); /* 只清 ncm */ + report_lyric_chunked("com.player2", 1, 0, "[00:01.00]Second app\n", MEOW_MEDIA_LRC_MAX); + s = meow_media_current(); + CHECK(s && !strcmp(s->app_id, "com.player2"), "L7: current == com.player2"); + CHECK(meow_media_lyric_valid(s) == 1, "L7: player2 歌词有效"); + CHECK(strcmp(meow_media_lyric_lrc(s), "[00:01.00]Second app\n") == 0, "L7: player2 歌词独立"); + + /* ---- 空歌词(无词歌曲):上报空串 → valid=0 ---- */ + report_lyric_chunked("com.player2", 2, 0, "", MEOW_MEDIA_LRC_MAX); + report_lyric_chunked("com.player2", 2, 1, "", MEOW_MEDIA_TLRC_MAX); + s = meow_media_current(); + CHECK(meow_media_lyric_valid(s) == 0, "空词上报 → lyric_valid == 0"); + + printf("\n%s (%d fails)\n", g_fails ? "FAILED" : "ALL PASS", g_fails); + return g_fails ? 1 : 0; +} diff --git a/patch_v2/meow_launcherd/util.c b/patch_v2/meow_launcherd/util.c index b226ba3..cafe692 100644 --- a/patch_v2/meow_launcherd/util.c +++ b/patch_v2/meow_launcherd/util.c @@ -1633,6 +1633,22 @@ static void ipc_dispatch(struct meow_ipc_msg *m) (long long)c.seek_ms, rc); break; } + /* 阶段D·批3:媒体会话歌词上报(App 播放时把 lrc/tlrc 上报到会话)。分片经 + * meow_media_lyric 归并(有界缓冲+seq 代际);回 ACK。 */ + case MSG_META_LYRIC: { + m_lyric_t l; + memset(&l, 0, sizeof l); + memcpy(&l, m->payload, m->plen < (uint16_t)sizeof l ? m->plen : (uint16_t)sizeof l); + l.app_id[MEOW_APP_ID_MAX - 1] = 0; + int rc = meow_media_lyric(&l); + m_ack_t a; + a.code = MEOW_OK; /* 会话策略:无会话丢弃(rc=-1),不视为错误 */ + a.status = (uint32_t)rc; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: META_LYRIC app=%s kind=%u seq=%u total=%u off=%u len=%u rc=%d -> ACK", + l.app_id, l.kind, l.seq, l.total, l.off, l.len, rc); + break; + } default: logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id); { diff --git a/patch_v2/ncm_app/app/play/ipc_media.c b/patch_v2/ncm_app/app/play/ipc_media.c index 3246c77..63eddb7 100644 --- a/patch_v2/ncm_app/app/play/ipc_media.c +++ b/patch_v2/ncm_app/app/play/ipc_media.c @@ -295,3 +295,47 @@ void ipc_media_stop(void) /* 线程在 200ms poll 内退出(detach,不 join)*/ g_thr_on = 0; } + +/* ---- 批3:live 歌词上报(lrc 原文 + tlrc 翻译 → 媒体会话,桌面歌词页实时滚动源)---- */ + +/* 歌词代际(换歌/换词自增;launcher 见新 seq 清旧词)。IC 线程外调用(lyric_worker), + * 单线程写(worker 是唯一调用者),无需锁。 */ +static uint32_t g_lyr_media_seq; + +/* 把一个 kind 的歌词文本按分片发到媒体会话(≤MEOW_LYRIC_CHUNK 一帧)。cmax=该 kind 上限: + * 0=MEOW_MEDIA_LRC_MAX,1=MEOW_MEDIA_TLRC_MAX。total=0(空串)时发一个空帧=清词。 */ +static void send_lyric_kind(int fd, uint32_t seq, uint16_t kind, const char *text, size_t cmax) +{ + if (!text) text = ""; + size_t total = strlen(text); + if (total > cmax) total = cmax; /* 接收侧缓冲上限:超限截断 */ + size_t off = 0; + do { + size_t n = total - off; + if (n > MEOW_LYRIC_CHUNK) n = MEOW_LYRIC_CHUNK; + m_lyric_t ly; + memset(&ly, 0, sizeof ly); + ncm_strlcpy(ly.app_id, "dev.player.ncm", sizeof ly.app_id); + ly.seq = seq; + ly.kind = kind; + ly.total = (uint16_t)total; + ly.off = (uint16_t)off; + ly.len = (uint16_t)n; + if (n) memcpy(ly.data, text + off, n); + ly.data[n] = 0; + send_frame(fd, MSG_META_LYRIC, seq, &ly, sizeof ly); + off += n; + } while (off < total); +} + +void ipc_media_lyric_report(const char *lrc, const char *tl) +{ + g_lyr_media_seq++; + int fd = connect_path(sock_path()); + if (fd < 0) { meow_log(MEOW_LOG_WARN, "ipc: lyric no launcher socket"); return; } + send_lyric_kind(fd, g_lyr_media_seq, 0, lrc, MEOW_MEDIA_LRC_MAX); /* 原文 */ + send_lyric_kind(fd, g_lyr_media_seq, 1, tl, MEOW_MEDIA_TLRC_MAX); /* 翻译 */ + close(fd); + meow_log(MEOW_LOG_INFO, "ipc: lyric reported seq=%u lrc=%zuB tlrc=%zuB", + g_lyr_media_seq, lrc ? strlen(lrc) : 0, tl ? strlen(tl) : 0); +} diff --git a/patch_v2/ncm_app/app/play/player_manager.c b/patch_v2/ncm_app/app/play/player_manager.c index feadc57..e1aae85 100644 --- a/patch_v2/ncm_app/app/play/player_manager.c +++ b/patch_v2/ncm_app/app/play/player_manager.c @@ -1045,8 +1045,14 @@ static void *lyric_worker(void *arg) pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu); continue; } + lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */ int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024); - if (rc == NCM_OK) lyric_parse(lrc, tl); + if (rc == NCM_OK) { + lyric_parse(lrc, tl); + ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */ + } else { + ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */ + } free(lrc); free(tl); pthread_mutex_lock(&g_lyrmu); diff --git a/patch_v2/ncm_app/bench/player_flow_sim.c b/patch_v2/ncm_app/bench/player_flow_sim.c index b68559c..db5bee3 100644 --- a/patch_v2/ncm_app/bench/player_flow_sim.c +++ b/patch_v2/ncm_app/bench/player_flow_sim.c @@ -58,6 +58,8 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz) } void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; } void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; } +/* core/ncm_common.c 提供 ncm_now_ms(墙钟毫秒);本 sim 不链 ncm_common.o,提供同语义桩。 */ +int64_t ncm_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec*1000 + ts.tv_nsec/1000000; } /* ---- appstate stub:窗内全命中,id=1000+idx;fee 可注入(§20 P8) ---- */ static int g_fee = 0; @@ -315,6 +317,8 @@ void audio_out_set_target(int ms) { (void)ms; } /* ---- 歌词(未被调用的 worker 会被 gc-sections 丢弃;lyric_clear 由 worker 用,防御性提供) ---- */ void lyric_clear(void) { } int lyric_parse(const char *a, const char *b) { (void)a; (void)b; return 0; } +/* 批3:player_manager 的 lyric_worker 会上报歌词到媒体会话;本 sim 不链 ipc_media.c,提供空桩。 */ +void ipc_media_lyric_report(const char *lrc, const char *tl) { (void)lrc; (void)tl; } static int g_fail; #define CHECK(cond, fmt, ...) do { \