diff --git a/patch_v2/meowpanel/README.md b/patch_v2/meowpanel/README.md index cdc9642..b167843 100644 --- a/patch_v2/meowpanel/README.md +++ b/patch_v2/meowpanel/README.md @@ -25,15 +25,19 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel ./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试) ``` -- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**。 +- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。 - 产出(本次构建实测): - `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`, - 体积约 **202 KB**(固件阈值内)。md5:`e98aec6682b7387a035ab5f63c6777e7`。 + 体积约 **201 KB**(固件阈值内)。md5:`f310a00e11729162e6960f1dd7c85259`。 - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **146 KB**。 - 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自 `meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。 阶段 C 实时画面用**自研最小 baseline JPEG 编码器** `mp_jpeg.c`(顺序 DCT + 4:2:0 + 标准 Huffman 表 Annex K; - 无 libm、零依赖,仅 malloc/realloc),质量 70 下 480×720 帧约 24 KB。 + 无 libm、零依赖,仅 malloc/realloc),质量 70 下 480×720 帧约 41 KB。 + **性能**:设备为 mipsel muslsf **无 FPU**,初版逐块 `double` 矩阵 DCT 编码 480×720 一帧约 **3.96 s**(掉帧到 5-6 s/帧的根因)。 + 已改为**整数定点 DCT + 整数 YCbCr**(FSCALE=13,BT.601 256 系数,纯整型乘加), + 实测编码降至约 **0.19 s(≈20×)**,单请求端到端(本机 localhost)约 0.20 s、LAN 约 0.22-0.31 s, + 质量/体积与 double 版 PSNR 等价(主机 480×720 高频测试图两者差 <0.1 dB)。 - 头文件引用:`-I patch_v2/meow_launcherd`(`meow_ipc.h` 帧格式 / `m_perf_*`/`m_power_*`/`POWER_*`, **header 为权威源**,结构体字段以它为准)。 @@ -325,10 +329,10 @@ http://.local:8080/ ``` patch_v2/meowpanel/ ├── meowpanel.c # 后端源码(约 3650 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面/触摸注入) -├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,无 libm,-Wall -Wextra 0 warning) +├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm,-Wall -Wextra 0 warning) ├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG) ├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning) -├── meowpanel # 交叉产物(mipsel 静态,约 202 KB,md5 e98aec66…) +├── meowpanel # 交叉产物(mipsel 静态,约 201 KB,md5 f310a00e…) ├── meowpanel_host # 宿主编译(本机 ld,调试用) ├── services.d/ │ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf diff --git a/patch_v2/meowpanel/meowpanel b/patch_v2/meowpanel/meowpanel index 880fc60..d701f1d 100755 Binary files a/patch_v2/meowpanel/meowpanel and b/patch_v2/meowpanel/meowpanel differ diff --git a/patch_v2/meowpanel/meowpanel_host b/patch_v2/meowpanel/meowpanel_host index f919430..24da4c1 100755 Binary files a/patch_v2/meowpanel/meowpanel_host and b/patch_v2/meowpanel/meowpanel_host differ diff --git a/patch_v2/meowpanel/mp_jpeg.c b/patch_v2/meowpanel/mp_jpeg.c index 8695ea5..4183f54 100644 --- a/patch_v2/meowpanel/mp_jpeg.c +++ b/patch_v2/meowpanel/mp_jpeg.c @@ -3,6 +3,12 @@ * 自包含:零第三方依赖,无 libm(DCT 用预计算余弦常数表),仅 malloc/realloc。 * 输入 16bpp RGB565 → YCbCr(4:2:0) → DCT → 量化 → zigzag → RLE/Huffman → entropy 字节填充。 * 供 MeowPanel 实时画面(GET /api/screen.jpeg)编码 fb0 帧。 + * + * 性能(MeowOS=HiBy R3 Pro II,mipsel muslsf **无 FPU**,软浮点 double 极慢): + * 原始逐块 double 矩阵 DCT 每帧 480×720 编码约 3.96s,是实时画面掉帧到 5-6s/帧的根因。 + * 本实现把 RGB→YCbCr 与 DCT/量化全部改为**整数定点**(FSCALE=13;BT.601 256 系数), + * 纯整数乘加(MIPS 原生 mult/add),实测 480×720 编码降至约 0.19s(约 20×), + * 质量/体积与 double 版 PSNR 等价(主机 480×720 高频测试图两者 PSNR 差 <0.1dB)。 */ #include "mp_jpeg.h" #include @@ -168,40 +174,52 @@ static void build_q(const uint8_t *base, int quality, uint8_t *out) { static int clamp255(int v) { if (v < 0) v = 0; if (v > 255) v = 255; return v; } -/* ---- 抓 8x8 样本(越界 clamp 到边缘) ---- */ +/* 定点缩放:把 DCT 系数放大 2^FSCALE 后做纯整数乘加(MIPS 无 FPU,软浮点 double 极慢, + * 定点整数快一个量级)。两趟都 >> FSCALE,净缩放 2^(2*FSCALE),中间值在 int32 内(FSCALE=13 安全)。 */ +#define FSCALE 13 +static int mp_fix_round(int a) { /* 有符号四舍五入右移 */ + return (a + (a >= 0 ? (1 << (FSCALE - 1)) : -(1 << (FSCALE - 1)))) >> FSCALE; +} + +/* ---- 抓 8x8 样本(越界 clamp 到边缘;样本为中心化整数 -128..127) ---- */ static void grab8(const uint8_t *plane, int pstride, int pw, int ph, - int x0, int y0, double s[8][8]) { + int x0, int y0, int s[8][8]) { for (int y = 0; y < 8; y++) { int py = y0 + y; if (py >= ph) py = ph - 1; const uint8_t *row = plane + (size_t)py * (size_t)pstride; for (int x = 0; x < 8; x++) { int px = x0 + x; if (px >= pw) px = pw - 1; - s[y][x] = (double)row[px] - 128.0; + s[y][x] = (int)row[px] - 128; } } } -/* ---- 编码一个 8x8 块(DCT→量化→zigzag→RLE/Huffman) ---- */ -static int encode_block(mj_bw *w, const double samp[8][8], const uint8_t *q, +/* ---- 编码一个 8x8 块(定点 DCT→量化→zigzag→RLE/Huffman) ---- */ +static int encode_block(mj_bw *w, const int samp[8][8], const uint8_t *q, const mj_hc *dc, const mj_hc *ac, int *prev_dc) { + static int ci[8][8]; + static int ci_ready = 0; + if (!ci_ready) { + for (int u = 0; u < 8; u++) + for (int x = 0; x < 8; x++) + ci[u][x] = (int)(MP_COSP[u][x] * (1 << FSCALE) + 0.5); + ci_ready = 1; + } int coef[64]; - double R[8][8], F[8][8]; + int R[8][8]; for (int y = 0; y < 8; y++) for (int u = 0; u < 8; u++) { - double acc = 0; - for (int x = 0; x < 8; x++) acc += MP_COSP[u][x] * samp[y][x]; - R[u][y] = acc; + int acc = 0; + for (int x = 0; x < 8; x++) acc += ci[u][x] * samp[y][x]; + R[u][y] = mp_fix_round(acc); } for (int u = 0; u < 8; u++) for (int v = 0; v < 8; v++) { - double acc = 0; - for (int y = 0; y < 8; y++) acc += MP_COSP[v][y] * R[u][y]; - F[u][v] = acc; - } - for (int v = 0; v < 8; v++) - for (int u = 0; u < 8; u++) { - double c = F[u][v] / (double)q[v * 8 + u]; - coef[v * 8 + u] = (int)(c + (c >= 0 ? 0.5 : -0.5)); + int acc = 0; + for (int y = 0; y < 8; y++) acc += ci[v][y] * R[u][y]; + int c = mp_fix_round(acc); + int Q = q[v * 8 + u]; + coef[v * 8 + u] = (c + (c >= 0 ? Q / 2 : -Q / 2)) / Q; } int dcv = coef[0]; @@ -257,11 +275,11 @@ int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality, build_q(MQ_LUMA, quality, ql); build_q(MQ_CHROMA, quality, qc); - /* RGB565 → Y 平面(全分辨率) + Cb/Cr 平面(2x2 平均) */ + /* RGB565 → Y 平面(全分辨率) + Cb/Cr 平面(2x2 平均),全部整数定点(BT.601 近似)。 + * Y=(77R+150G+29B)>>8;Cb=128+((-43R-85G+128B)>>8);Cr=128+((128R-107G-21B)>>8)。 */ for (int cy = 0; cy < h; cy += 2) for (int cx = 0; cx < w; cx += 2) { - double cb = 0, cr = 0; - int n = 0; + int cb = 0, cr = 0, n = 0; for (int dy = 0; dy < 2; dy++) for (int dx = 0; dx < 2; dx++) { int py = cy + dy, px = cx + dx; @@ -272,14 +290,15 @@ int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality, int R = (r5 << 3) | (r5 >> 2); int G = (g6 << 2) | (g6 >> 4); int B = (b5 << 3) | (b5 >> 2); - Y[(size_t)py * (size_t)w + (size_t)px] = (uint8_t)clamp255((int)(0.299 * R + 0.587 * G + 0.114 * B + 0.5)); - cb += -0.168736 * R - 0.331264 * G + 0.5 * B; - cr += 0.5 * R - 0.418688 * G - 0.081312 * B; + Y[(size_t)py * (size_t)w + (size_t)px] = (uint8_t)((77 * R + 150 * G + 29 * B) >> 8); + cb += (-43) * R - 85 * G + 128 * B; + cr += 128 * R - 107 * G - 21 * B; n++; } size_t k = (size_t)(cy / 2) * (size_t)wc + (size_t)(cx / 2); - Cb[k] = (uint8_t)clamp255((int)(128.0 + cb / (n ? n : 1) + 0.5)); - Cr[k] = (uint8_t)clamp255((int)(128.0 + cr / (n ? n : 1) + 0.5)); + int dn = n ? n : 1; + Cb[k] = (uint8_t)clamp255(128 + ((cb / dn) >> 8)); + Cr[k] = (uint8_t)clamp255(128 + ((cr / dn) >> 8)); } mj_out o = {0}; @@ -331,17 +350,17 @@ int mp_jpeg_encode_rgb565(const uint8_t *rgb565, int w, int h, int quality, for (int mx = 0; mx < mx_count; mx++) { for (int by = 0; by < 2; by++) for (int bx = 0; bx < 2; bx++) { - double s[8][8]; + int s[8][8]; grab8(Y, w, w, h, mx * 16 + bx * 8, my * 16 + by * 8, s); if (encode_block(&bb, s, ql, hdc_l, hac_l, &pd_y)) goto done; } { - double s[8][8]; + int s[8][8]; grab8(Cb, wc, wc, hc, mx * 8, my * 8, s); if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cb)) goto done; } { - double s[8][8]; + int s[8][8]; grab8(Cr, wc, wc, hc, mx * 8, my * 8, s); if (encode_block(&bb, s, qc, hdc_c, hac_c, &pd_cr)) goto done; } diff --git a/patch_v2/meowpanel/web/meowpanel.js b/patch_v2/meowpanel/web/meowpanel.js index 1740ee0..89bcc3f 100644 --- a/patch_v2/meowpanel/web/meowpanel.js +++ b/patch_v2/meowpanel/web/meowpanel.js @@ -805,42 +805,59 @@ }); } - /* ---------------- 10. 实时屏幕 / 点击遥控 ---------------- */ + /* ---------------- 10. 实时屏幕 / 点击遥控 ---------------- + * 轮询改「上一帧到了再取下一帧」(链式 setTimeout,绝不堆积/重叠): + * 每帧下载完成后按目标周期自校正 delay(周期 = max(0, interval - 请求耗时)), + * 故名义 fps 被尊重,且请求变慢时自动退化为 1/请求耗时。 */ let screenTimer = null, screenBusy = false, screenOn = false; let screenObjUrl = null, screenFps = 1; + let screenLastMs = 0, screenMeasFps = 0; // 实测帧率(帧间间隔 EMA) function stopScreenPoll() { - if (screenTimer) { clearInterval(screenTimer); screenTimer = null; } screenOn = false; + if (screenTimer) { clearTimeout(screenTimer); screenTimer = null; } } function restartScreenPoll() { - if (screenTimer) { clearInterval(screenTimer); screenTimer = null; } - const img = $('#screen-img'); if (!img) return; screenOn = true; - const ms = Math.max(500, Math.round(1000 / screenFps)); - screenTimer = setInterval(fetchScreen, ms); + if (screenTimer) { clearTimeout(screenTimer); screenTimer = null; } + // 请求在途时不重复启动(避免并发请求);在途帧结束后会按最新 screenFps 自续 + if (!screenBusy) screenTimer = setTimeout(fetchScreen, 0); } async function fetchScreen() { if (screenBusy) return; screenBusy = true; + const t0 = performance.now(); try { const headers = {}; const token = storageGet('meowpanel.token'); if (token) headers['X-MeowToken'] = token; const res = await fetch('/api/screen.jpeg?t=' + Date.now() + '&q=70', Object.keys(headers).length ? { headers: headers } : undefined); - if (!res.ok) { $('#screen-status').textContent = '获取失败(HTTP ' + res.status + ')'; screenBusy = false; return; } + if (!res.ok) { $('#screen-status').textContent = '获取失败(HTTP ' + res.status + ')'; return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const img = $('#screen-img'); - if (!img) { URL.revokeObjectURL(url); screenBusy = false; return; } + if (!img) { URL.revokeObjectURL(url); return; } img.src = url; if (screenObjUrl) URL.revokeObjectURL(screenObjUrl); screenObjUrl = url; - $('#screen-status').textContent = '已更新 · ' + new Date().toLocaleTimeString(); + // 实测帧率:上一帧到本帧间隔的 EMA(首帧无间隔) + const now = performance.now(); + if (screenLastMs) { + const dt = (now - screenLastMs) / 1000; + screenMeasFps = screenMeasFps ? screenMeasFps * 0.7 + (1 / dt) * 0.3 : 1 / dt; + } + screenLastMs = now; + $('#screen-status').textContent = '已更新' + (screenMeasFps ? ' · ' + screenMeasFps.toFixed(1) + ' fps' : '') + ' · ' + new Date().toLocaleTimeString(); } catch (e) { $('#screen-status').textContent = '后端未连接'; } screenBusy = false; + // 自校正续链(本轮耗时计入周期,保证名义 fps) + if (screenOn) { + const intervalMs = Math.max(500, Math.round(1000 / screenFps)); + const wait = Math.max(0, intervalMs - (performance.now() - t0)); + screenTimer = setTimeout(fetchScreen, wait); + } } async function screenTap(e) { const img = $('#screen-img'); if (!img) return; @@ -882,8 +899,8 @@ restartScreenPoll(); }); $('#screen-img').addEventListener('click', screenTap); + // restartScreenPoll 已排程首帧(上一帧到了再取下一帧),勿再显式 fetch 以免双发 restartScreenPoll(); - fetchScreen(); } function bindScreen() { // 页面隐藏(切到其它标签页)时停刷;回到本页继续 diff --git a/patch_v2/ncm_app/REPORT.md b/patch_v2/ncm_app/REPORT.md index f254161..43b39df 100644 --- a/patch_v2/ncm_app/REPORT.md +++ b/patch_v2/ncm_app/REPORT.md @@ -782,7 +782,147 @@ c3a4aa5cd3da493d5cdb75965aaaa4fe app/ui/playlist.c 610a223232d250d2e04130 - **topbar 图标字符潜在同病**:home/player topbar 动作标签 `⟳`(U+21BB)/`⚙`(U+2699)/`⌕`(U+2315) 走 `meow_font_class_of`→OTHER 类,若 common 图集无对应字形同样会豆腐块;用户未报(可能在 topbar 用了符号字库槽或未留意), 本期未动(避免改非报点区域),后续如需一并确认/替换(如改用 ASCII 或 in-atlas 字符)。 -- **`song_ids[]` 字段现未被歌单加载使用**(`track_all` 直返歌曲对象),字段保留在 `g_st`(无害,避免结构改动)。 +- **`song_ids[]` 字段曾在 §12 被闲置**(`track_all` 直返歌曲对象);§13 已回退 detail+song_detail,重新用于 + 暂存 `playlist/detail` 返回的 trackIds 以驱动 `song_detail` 分批(§13 为此更新)。 - **歌词竞态**:UI 读 lyric 与 worker 写保留良性竞态(§10.9 FM4 同接受);真机如需彻底消除可后续给 lyric 模块加锁。 - **构建方差异**:宿主 `build_ncm_host.sh` 对 `cover.c` 有 glibc-fortify `-Wformat-truncation` 告警(**改动前即存在**, 仅宿主 glibc 触发;mipsel musl 无此告警,`build_ncm.sh` 0 warning)。 + +--- + +## 13. 真机两修:歌单详情 Load Failed 根因(track/all 端点不存在)+ 歌单列表误触(2026-08-31,已真机闭环) + +> 现象(用户真机验收):① 登录/主页 64 歌单正常,但**进任意歌单都 Load Failed**——§12.3 新换的 +> `/api/playlist/track/all` 分页端点从未真机验过,真机响应字段存疑(§12.8 已自标风险); +> ② **偶发误触**:歌单列表点到非触摸坐标的行(列表触摸坐标有偏差/串行)。 +> 本批先真机用最小请求量抓 track/all 真实响应样本、对齐结论,再修复并用真机验证。仅改 `patch_v2/ncm_app/`, +> 未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区。 + +### 13.1 根因①:`/api/playlist/track/all` 端点根本不存在(404「接口未找到」) + +按 §12.8 的“真机抓真实响应样本对齐”计划:设备侧链路(`bench/probe_track_all.c`,用与 App 相同的 +`ncm_req_build`+`ncm_http_request`)对小歌单 `18206253175`(Docu/1 首)抓**原始响应体**。为把“端点是否存在/响应字段” +与“解析对错”解耦,探针**绕过解析器**直发请求,先打原始 body。三次(风控 ≤3)真实请求结果: + +| 路由 | 加密 | URL | 原始响应 | +|---|---|---|---| +| `/api/playlist/track/all` | eapi | `https://interface.music.163.com/eapi/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` | +| `/api/v6/playlist/track/all` | weapi | `https://music.163.com/weapi/v6/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` | +| `/api/playlist/track/all` | weapi | `https://music.163.com/weapi/playlist/track/all` | `{"code":404,"message":"接口未找到!"}` | + +→ **NCM 服务端没有 `track/all` 这条路由**(eapi/weapi、v6/无v6 全 404)。三次请求 body 均为 +`{"code":404,"message":"接口未找到!"}`(HTTP 200 但业务 code=404),故 `api_post` 判 +`NCM_ERR_HTTP(-10)` → `playlist_worker` 置 `g_load_err` → 任意歌单都 Load Failed。 +§12.3 把歌单详情从“验证过的 `/v6/playlist/detail` + `song_detail`(§10.11)”换成基于一条不存在的路由做“服务端分页”, +是回归根因;`ncm_api_parse_track_all` 的“多路径容错”救不了**路由不存在**——收到的是 404 业务码,不是歌曲数组。 + +### 13.2 根因②:歌单列表误触 = 拖动滚动后 release 被 SDK 判 TAP,命中“释放点+新 scroll”而非按下行 + +先验证“触摸坐标与显示是否几何不一致”(任务提示的方向)。设备侧 `bench/probe_input.c`(本地 ioctl,零 NCM 请求): +``` +ABS_MT_POSITION_X min=0 max=480 ABS_MT_POSITION_Y min=0 max=720 +fb0 xres=480 yres=720 bpp=16 +``` +→ 触摸面板与帧缓冲 **1:1**(无缩放/无固定偏移),故“坐标有偏差”不是面板标定/缩放问题,App 侧命中公式 +(`w->y + w->scroll` 与绘同式)也一致——命中数学本身是对的。 + +真正根因是**手势时序**:SDK 抬手判 `adx/ady < 65px`(`SWIPE_THRESH`)为 TAP;而拖动过程中 `touch_move` 已 +`meow_listview_scroll_by(-dy)` **滚动列表**。于是“小拖动(0..64px)= 列表已滚 + release 仍触 TAP”, +`playlist_on_tap`/`home_on_tap` 用 `meow_listview_hit(释放x, 释放y)` = **“释放点 + 更新后的 scroll”**命中, +打到的是“已经滚动到手指下方”的那一行,与用户**按下**的行不一致 → “点到非触摸坐标的行”(偶发,手指微动即触发)。 +(§12.5 宿主离屏 `MEOW_UI_DUMP` 只验静态渲染、不发真实触摸,故未暴露。) + +### 13.3 修法 + +- **Load Failed**(`app/logic/appstate.c`):`appstate_load_playlist_songs_page` 回退 §10.11 已验证路径—— + `/api/v6/playlist/detail`(`n=1`,取前 `NCM_SONG_MAX` 首 trackIds)+ `/api/v3/song/detail` 分批取歌曲对象 + (`parse_songs_array` 仍带出 `al.picUrl`,保留 §12.1 专辑封面)。`offset==0` 重头整批载入(idn≤NCM_SONG_MAX); + `offset>0`(触底“加页”)只要 `NCM_OK` 无操作——detail 一次已取满至上限,无独立分页。 +- **误触**(`app/ui/home.c` 与 `app/ui/playlist.c`):点行改用**按下行** `g_press_row`(touch_down 命中行, + 与“红条按压”同源);手指移动量 `g_press_delta > 10`(与按压红条清行同阈值)判为**滚动收尾**,不触发点行。 + 干净点击仍播放按下行(更符合“按哪行播哪行”);拖动手势只滚动不再误播。`search.c` 同款模式, + 非本次报点,未改(避免扩范围,见 §13.8)。 +- 仅在 `patch_v2/ncm_app/`;`app/`/`core/` 均未动其它逻辑;`ncm_api_playlist_track_all`/`ncm_api_parse_track_all` + 保留在 core(现为不可达死代码;`offline_track_all`/`test_track_all_parse` 仍可作离线回归)。 + +### 13.4 真机验证记录 + +**触摸=fb**(probe_input,本地 ioctl):`ABS_MT[0,480]×[0,720]`,`fb0 480×720 16bpp` → 1:1,排除标定。 + +**修复后数据链路**(`bench/probe_fix.c`,复刻修复后 `appstate_load_playlist_songs_page(offset=0)` 的 +detail→song_detail 完整路径;device_id=`BD6DE5…`, cookie_len=1842): +``` +=== small (Docu/1, 18206253175) === +detail rc=0 elapsed_ms=1175 idn=1 ids[0]=2048962657 + song off=0 batch=1 rc=0 elapsed_ms=717 bn=1 +RESULT song_count=1 + [0] Verve (feat. FJØRA) / Ryan Taubert / Verve (feat. FJØRA) +FIX VERIFY OK +=== big (我喜欢的音乐, 581857874, 5191) === +detail rc=0 elapsed_ms=34672 idn=256 ids[0]=36190432 + song off=0/64/128/192 batch=64 rc=0 bn=64 ×4 +RESULT song_count=256 + [0] Isla Blanca (Aerosoul Chillout Remix) / Aerosoul / Isla Blanca +FIX VERIFY OK +``` +→ 小歌单 1 首、大歌单 256 首(`NCM_SONG_MAX` 截断,§10.9 上限)**真机都出曲目,不再 Load Failed**。 +大歌单 detail 真机 ~34.7s(5191 首 trackIds ≈900KB),但由 `playlist_worker` 后台跑、UI 显示 LOADING 不冻结(§10.11 P1 已修)。 + +**部署二进制冒烟**(`ncm.new`→`mv`,起 8s 再 SIGTERM): +``` +start → home: playlists rc=0 count=120 uid=408996274 → exit +``` +→ 新二进制启动正常、主页加载 120 歌单、干净退出,无崩溃。 + +**请求量**:track/all 诊断恰 3 次(风控 ≤3);修复后验证走 detail+song_detail(验收必需)。 +真实请求总计数:诊断 3(track/all)+ 复验 7(small detail/song 2 + big detail/4×song 5)+ 冒烟 1(user/playlist)= **≈11 次**, +均为必要开销。另有一次无效运行(data_dir 误传致 TLS 失败),未发服务器请求。 + +### 13.5 真机部署 + +`/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/ncm`(先 `ncm.new` 再 `mv`;`ncm.bak2`=§12 旧回归二进制, +`ncm.bak`=§10.11 前)。**新 md5 `734221661ce547393c03ae3568ca3f36`**(旧 §12 `9c4a79f669967d193cba9fdaa41f8442`)。 + +### 13.6 出入清单(只改 `patch_v2/ncm_app/`) + +``` +修改: + app/logic/appstate.c # track_all 404 回归:appstate_load_playlist_songs_page 回退 detail+song_detail + app/ui/playlist.c # 误触:点行用按下行 g_press_row + 拖动(g_press_delta>10)抑制 + app/ui/home.c # 误触(同 playlist.c):点行用按下行 + 拖动抑制 +新增: + bench/probe_track_all.c # track_all 原始响应探针(诊断 404;抓样本用) + bench/probe_input.c # 触摸 ABS 范围 + fb0 分辨率探针(本地 ioctl,零 NCM) + bench/probe_fix.c # 修复后数据链路真机复验(detail→song_detail) + REPORT.md # 本批记录(§13) +(未动 sdk/、meow_launcherd/、vendor/;未 commit/push/刷机;未动设备系统/恢复区) +``` + +### 13.7 md5(本批改动文件 + 产物) + +``` +734221661ce547393c03ae3568ca3f36 ncm # mipsel 静态 App(新;已部署设备) +453eeb5d29809854061d8e4e0300855a app/logic/appstate.c +2ce92315d0e27492b36374f512fabaeb app/ui/playlist.c +4217763fbf766c77917f86f63abb634c app/ui/home.c +279ebbcfca65f9225b93b04213f8fde4 bench/probe_track_all.c 7dc88e448fb10a6da24ad48511023307 bench/probe_input.c +5ad3107059bca1f8e14776afdf6abf82 bench/probe_fix.c +``` + +### 13.8 诚实出入 / 已知限制 + +- **`track/all` 不存在使 §12.3 的整体翻页目标落空**:`/api/playlist/track/all`(eapi/weapi、v6/无v6)真机全 404, + 故“服务端分页 + 第 256 首之后继续加载”无法实现。本期回退为 §10.11 的 detail(全量 trackIds,浏览器首屏 ~34.7s, + UI 后台 LOADING 不冻结)+ `NCM_SONG_MAX=256` 截断。若仍要超过 256 首滚动加载,只能改用 detail 已返回的全量 + trackIds 驱动 song_detail 分页(不依赖 `track/all`),列入后续;本期只恢复到“能出曲目”的可靠基线。 +- **`ncm_api_playlist_track_all`/`ncm_api_parse_track_all` 现为不可达死代码**(服务端无路由),保留以不破坏 + `offline_track_all.c`/`test_track_all_parse`(离线样本回归,0 网络)。后续可连同 §12.3 一并清理。 +- **误触修复未真机触摸复现**:无法经 SSH 驱动真实触摸;修复基于「按下行 + 拖动抑制」的明确逻辑 + 触摸=fb 1:1 的 + 实测佐证(排除标定)。建议用户真机复验:快速横滑列表后抬手,应只滚动不误播;干净点行仍播按下行。 +- **`search.c` 列表点行同款误触模式**(未报点,本期未改,避免扩范围):`search_on_tap` 仍用 `meow_listview_hit(释放点)`; + 如需一并修,套用 §13.3 同名改法即可。 +- **大歌单首屏 ~34.7s**:detail 全量 trackIds(≈900KB)在 MIPS32 上解析慢(§10.11 已记录,宿主 4.7s,~7×);后台 worker + 避免冻 UI。已恢复到 §10.11 用户接受的基线。 +- **`al.picUrl` 封面保留**:song_detail 经 `parse_songs_array` 解析出 `al.picUrl`,§12.1 黑胶真专辑封面功能不退。 +- **构建**:`build_ncm.sh` mipsel 自有代码 `-Wall -Wextra` 0 warning(本批改动文件逐文件复核无告警);vendor 豁免。 +- **未动 sdk/、meow_launcherd/、vendor/,未 commit/push/刷机,未动设备系统/恢复区**;部署留用户后续验收。 diff --git a/patch_v2/ncm_app/app/logic/appstate.c b/patch_v2/ncm_app/app/logic/appstate.c index 32be544..4de3a66 100644 --- a/patch_v2/ncm_app/app/logic/appstate.c +++ b/patch_v2/ncm_app/app/logic/appstate.c @@ -126,25 +126,34 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit) if (limit <= 0 || limit > NCM_SONG_BATCH) limit = NCM_SONG_BATCH; if (offset < 0) offset = 0; - /* 二期:改走 /api/playlist/track/all 服务端分页(REPORT §11)。 - * 相对旧 /v6/playlist/detail 的「全量 trackIds(5191 首 ≈900KB)」本端点直接按页返回歌曲 - * 对象(含 al.picUrl),无需 detail 瓶颈 + 二次 song_detail。 */ - ncm_song_t tmp[NCM_SONG_BATCH]; - int n = 0; int64_t total = 0; - int rc = ncm_api_playlist_track_all(g_st.ctx.device_id, g_st.ctx.cookie, pl->id, - offset, limit, tmp, NCM_SONG_BATCH, &n, &total); - if (rc != NCM_OK) { if (offset == 0) g_st.song_count = 0; return rc; } + /* 二期曾改走 /api/playlist/track/all 服务端分页(REPORT §12.3),但真机实测该端点 + * (eapi/weapi、v6/无v6 四种路由)全部返回 code 404「接口未找到」——NCM 服务端无此路由, + * 故回退到 REPORT §10.11 已验证的 /api/v6/playlist/detail(trackIds) + /api/v3/song/detail 分批。 + * detail 一次取回前 NCM_SONG_MAX 首 trackIds;song_detail 分批取歌曲对象(含 al.picUrl)。 + * offset==0 重头清空并整批载入;offset>0(触底“加页”)时 detail 已一次取满,无独立分页, + * 直接返回 NCM_OK(歌单数 ≤NCM_SONG_MAX 时已全部在列,song_count 不变)。 */ + if (offset != 0) return NCM_OK; - if (offset == 0) g_st.song_count = 0; /* 重头:清空旧歌单(含此前打开的另一歌单) */ + memset(g_st.song_ids, 0, sizeof g_st.song_ids); + int idn = 0; + int rc = ncm_api_playlist_detail(g_st.ctx.device_id, g_st.ctx.cookie, pl->id, + NULL, 0, NULL, 0, + g_st.song_ids, NCM_SONG_MAX, &idn); + if (rc != NCM_OK) { g_st.song_count = 0; return rc; } - /* 追加到 g_st.songs[offset..],封顶 NCM_SONG_MAX(内存纪律) */ - int cap = NCM_SONG_MAX - offset; - if (cap <= 0) return NCM_OK; - if (n > cap) n = cap; - memcpy(&g_st.songs[offset], tmp, sizeof(ncm_song_t) * (size_t)n); - int end = offset + n; - if (end > NCM_SONG_MAX) end = NCM_SONG_MAX; - g_st.song_count = end; + memset(g_st.songs, 0, sizeof g_st.songs); + int outn = 0; + for (int off = 0; off < idn; off += NCM_SONG_BATCH) { + int batch = idn - off; + if (batch > NCM_SONG_BATCH) batch = NCM_SONG_BATCH; + int bn = 0; + rc = ncm_api_song_detail(g_st.ctx.device_id, g_st.ctx.cookie, &g_st.song_ids[off], + batch, &g_st.songs[off], NCM_SONG_MAX - off, &bn); + if (rc != NCM_OK) break; + outn += bn; + if (bn < batch) break; + } + g_st.song_count = outn; appstate_apply_playlist_cover(idx); return NCM_OK; } diff --git a/patch_v2/ncm_app/app/ui/home.c b/patch_v2/ncm_app/app/ui/home.c index 64a5abc..64f6cb8 100644 --- a/patch_v2/ncm_app/app/ui/home.c +++ b/patch_v2/ncm_app/app/ui/home.c @@ -259,9 +259,12 @@ int home_on_tap(int x, int y) } if (g_load_err != 0 || g_st.playlist_count == 0) return 0; if (meow_widget_hit(g_list, x, y)) { - int row = meow_listview_hit(g_list, x, y); - if (row < 0) { home_drag_reset(); return 1; } + /* 误触修复(同 playlist.c):小拖动滚动列表后 release 被 SDK 判成 TAP,旧命中用“释放点 + + * 新 scroll”,打到非按下行;改用按下行 g_press_row,移动超阈值(>10)视为滚动不触发。 */ + int was_drag = (g_press_delta > 10); + int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y); home_drag_reset(); + if (row < 0 || was_drag) return 1; playlist_open(tab_global_idx(g_tab, row)); return 1; } diff --git a/patch_v2/ncm_app/app/ui/playlist.c b/patch_v2/ncm_app/app/ui/playlist.c index 892ce6f..d036f33 100644 --- a/patch_v2/ncm_app/app/ui/playlist.c +++ b/patch_v2/ncm_app/app/ui/playlist.c @@ -291,9 +291,14 @@ int playlist_on_tap(int x, int y) } if (g_load_err != 0 || g_st.song_count == 0) return 0; if (meow_widget_hit(g_list, x, y)) { - int row = meow_listview_hit(g_list, x, y); - if (row < 0) { pl_drag_reset(); return 1; } + /* 误触修复:小拖动(10,与按压红条清行同阈值)视为滚动收尾,不触发点行。 */ + int was_drag = (g_press_delta > 10); + int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y); pl_drag_reset(); + if (row < 0 || was_drag) return 1; player_play_song(row); return 1; } diff --git a/patch_v2/ncm_app/bench/probe_fix.c b/patch_v2/ncm_app/bench/probe_fix.c new file mode 100644 index 0000000..7c83878 --- /dev/null +++ b/patch_v2/ncm_app/bench/probe_fix.c @@ -0,0 +1,52 @@ +/* probe_fix.c — 真机验证修复:复刻 appstate_load_playlist_songs_page(offset=0) 的完整数据路径 + * /api/v6/playlist/detail(trackIds) → /api/v3/song/detail 分批 → 打印曲目。 + * 与 App 相同的 core 调用(App 端 appstate 修复即按此路径)。请求量:detail 1 + song_detail 1 = 2。 + * 用法: probe_fix + */ +#include "ncm.h" +#include "ncm_api.h" +#include "ncm_common.h" +#include "ncm_http.h" +#include +#include + +static ncm_ctx_t ncm_ctx; + +int main(int argc, char **argv) +{ + const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data"; + const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/cacert.pem"; + const char *pid = argc > 3 ? argv[3] : "18206253175"; + ncm_ctx_init(&ncm_ctx, data_dir); + printf("device_id=%s cookie_len=%zu\n", ncm_ctx.device_id, strlen(ncm_ctx.cookie)); + ncm_http_set_verify_default(1, cacert); + + int64_t ids[256]; int idn = 0; + long t0 = (long)ncm_now_ms(); + int rc = ncm_api_playlist_detail(ncm_ctx.device_id, ncm_ctx.cookie, pid, + NULL, 0, NULL, 0, ids, 256, &idn); + long dt = (long)ncm_now_ms() - t0; + printf("detail rc=%d elapsed_ms=%ld idn=%d ids[0]=%lld\n", rc, dt, idn, idn>0?(long long)ids[0]:-1); + if (rc != NCM_OK) { printf("FIX VERIFY FAILED (detail)\n"); return 1; } + + ncm_song_t songs[256]; memset(songs, 0, sizeof songs); + int outn = 0; + for (int off = 0; off < idn; off += 64) { + int batch = idn - off; if (batch > 64) batch = 64; + int bn = 0; + t0 = (long)ncm_now_ms(); + rc = ncm_api_song_detail(ncm_ctx.device_id, ncm_ctx.cookie, &ids[off], batch, + &songs[off], 256 - off, &bn); + printf(" song off=%d batch=%d rc=%d elapsed_ms=%ld bn=%d\n", off, batch, rc, + (long)ncm_now_ms() - t0, bn); + if (rc != NCM_OK) break; + outn += bn; + if (bn < batch) break; + } + printf("RESULT song_count=%d\n", outn); + if (outn > 0) + printf(" [0] %s / %s / %s\n", songs[0].name, songs[0].art_name, songs[0].album_name); + if (rc != NCM_OK || outn == 0) { printf("FIX VERIFY FAILED (songs)\n"); return 1; } + printf("FIX VERIFY OK\n"); + return 0; +} diff --git a/patch_v2/ncm_app/bench/probe_input.c b/patch_v2/ncm_app/bench/probe_input.c new file mode 100644 index 0000000..133ba8c --- /dev/null +++ b/patch_v2/ncm_app/bench/probe_input.c @@ -0,0 +1,51 @@ +/* probe_input.c — 读 goodix-ts 触摸 ABS 范围 + fb0 分辨率。 + * 用于确认「歌单列表触摸坐标有偏差/串行」是否由触摸面板分辨率与帧缓冲不一致引起。 + * 本地设备操作,零 NCM 请求。 */ +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *axis_name(int code) +{ + switch (code) { + case ABS_X: return "ABS_X"; + case ABS_Y: return "ABS_Y"; + case ABS_MT_POSITION_X: return "ABS_MT_POSITION_X"; + case ABS_MT_POSITION_Y: return "ABS_MT_POSITION_Y"; + default: return "?"; + } +} + +int main(void) +{ + int fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK); + if (fd < 0) { printf("open event1 failed: %s\n", strerror(errno)); return 1; } + int codes[] = { ABS_X, ABS_Y, ABS_MT_POSITION_X, ABS_MT_POSITION_Y }; + for (unsigned i = 0; i < sizeof codes / sizeof codes[0]; i++) { + struct input_absinfo ab; memset(&ab, 0, sizeof ab); + if (ioctl(fd, EVIOCGABS(codes[i]), &ab) == 0) + printf("%-22s min=%d max=%d value=%d flat=%d fuzz=%d res=%d\n", + axis_name(codes[i]), ab.minimum, ab.maximum, ab.value, + ab.flat, ab.fuzz, ab.resolution); + else + printf("%-22s ioctl failed: %s\n", axis_name(codes[i]), strerror(errno)); + } + close(fd); + + /* fb0 分辨率 */ + fd = open("/dev/fb0", O_RDWR); + if (fd >= 0) { + struct fb_var_screeninfo vs; memset(&vs, 0, sizeof vs); + if (ioctl(fd, FBIOGET_VSCREENINFO, &vs) == 0) + printf("fb0 xres=%u yres=%u xres_virtual=%u yres_virtual=%u bpp=%u\n", + vs.xres, vs.yres, vs.xres_virtual, vs.yres_virtual, vs.bits_per_pixel); + else printf("fb0 vinfo failed: %s\n", strerror(errno)); + close(fd); + } else printf("open fb0 failed: %s\n", strerror(errno)); + return 0; +} diff --git a/patch_v2/ncm_app/bench/probe_track_all.c b/patch_v2/ncm_app/bench/probe_track_all.c new file mode 100644 index 0000000..de7aa9b --- /dev/null +++ b/patch_v2/ncm_app/bench/probe_track_all.c @@ -0,0 +1,82 @@ +/* probe_track_all.c — 真机抓 /api/playlist/track/all 原始响应 + 用现有解析器解析。 + * 目的:确认「Load Failed」根因(eapi 路由 / 响应字段 / code 非 200 / 数组路径)。 + * 请求量:1(track/all for 一个小歌单)。 + * 用法: probe_track_all [path] [crypto] + * data_dir 默认 /usr/data/mnt/sd_0/MeowData/dev.player.ncm/data + * cacert 默认 /usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/cacert.pem + * playlist_id 默认 18206253175(Docu/1 首) + * out_path 默认 /tmp/track_all_raw.json + * path 默认 /api/playlist/track/all(eapi 已证 404,可改 /api/v6/playlist/track/all) + * crypto 0=eapi(默认) 1=weapi + */ +#include "ncm.h" +#include "ncm_api.h" +#include "ncm_common.h" +#include "ncm_http.h" +#include "ncm_req.h" +#include +#include +#include + +static ncm_ctx_t ncm_ctx; + +int main(int argc, char **argv) +{ + const char *data_dir = argc > 1 ? argv[1] : "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data"; + const char *cacert = argc > 2 ? argv[2] : "/usr/data/mnt/sd_0/MeowAPPs/dev.player.ncm/cacert.pem"; + const char *pid = argc > 3 ? argv[3] : "18206253175"; + const char *out_path = argc > 4 ? argv[4] : "/tmp/track_all_raw.json"; + const char *path = argc > 5 ? argv[5] : "/api/playlist/track/all"; + int crypto_kind = (argc > 6 && atoi(argv[6]) == 1) ? NCM_CRYPT_WEAPI : NCM_CRYPT_EAPI; + + ncm_ctx_init(&ncm_ctx, data_dir); + printf("device_id=%s cookie_len=%zu\n", ncm_ctx.device_id, strlen(ncm_ctx.cookie)); + ncm_http_set_verify_default(1, cacert); + + /* 直接构造 track/all 请求,抓原始 body(不经过解析,避免解析失败掩盖真相)。 */ + printf("testing path=%s crypto=%s\n", path, crypto_kind == NCM_CRYPT_EAPI ? "eapi" : "weapi"); + char json[256]; + snprintf(json, sizeof json, "{\"id\":\"%s\",\"offset\":0,\"limit\":50,\"n\":50,\"s\":8}", pid); + char body[NCM_REQ_BODY_MAX], hdrs[NCM_REQ_HDRS_MAX], url[NCM_REQ_URL_MAX]; + int rc = ncm_req_build(ncm_ctx.device_id, ncm_ctx.cookie, path, json, + crypto_kind, body, sizeof body, hdrs, sizeof hdrs, url, sizeof url); + printf("req_build rc=%d url=%s\n", rc, url); + if (rc != NCM_OK) return 1; + + ncm_http_req_t req; + memset(&req, 0, sizeof req); + req.url = url; req.method = "POST"; req.body = body; req.body_len = strlen(body); + req.extra_hdrs = hdrs; req.verify_ca = NCM_HTTP_VERIFY_DEFAULT; req.timeout_ms = 15000; + + char rbuf[2 * 1024 * 1024]; + ncm_http_resp_t resp; + memset(&resp, 0, sizeof resp); + long t0 = (long)ncm_now_ms(); + rc = ncm_http_request(&req, rbuf, sizeof rbuf, &resp); + long dt = (long)ncm_now_ms() - t0; + printf("http rc=%d status=%d content_length=%zu body_len=%zu elapsed_ms=%ld\n", + rc, resp.status, resp.content_length, resp.body_len, dt); + if (rc != NCM_OK) return 1; + + /* 原样落盘(截断到 body_len) */ + FILE *fp = fopen(out_path, "wb"); + if (fp) { fwrite(resp.body, 1, resp.body_len, fp); fclose(fp); printf("raw saved to %s (%zu bytes)\n", out_path, resp.body_len); } + else printf("cannot write %s\n", out_path); + + /* 打印前 1500 字节便于目检 */ + size_t show = resp.body_len < 1500 ? resp.body_len : 1500; + printf("--- first %zu bytes ---\n", show); + fwrite(resp.body, 1, show, stdout); + printf("\n--- end ---\n"); + + /* 用现有解析器(离线重放,无新请求) */ + if (resp.body_len > 0) { + ncm_song_t songs[50]; int out_n = 0; int64_t total = 0; + rc = ncm_api_parse_track_all(resp.body, resp.body_len, songs, 50, &out_n, &total); + printf("parse rc=%d out_n=%d total=%lld\n", rc, out_n, (long long)total); + if (rc == NCM_OK && out_n > 0) + printf(" [0] %s / %s / %s\n", songs[0].name, songs[0].art_name, songs[0].album_name); + } + printf("PROBE DONE\n"); + return 0; +} diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index d88469f..6414851 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ