ReconnectPolicyTest(11 项)验的是**纯函数**「给定关闭码应该怎么退避」,而 ⑦
「连接频繁重连」的缺陷往往出在**调用点**。新增 PoseWebSocketReconnectTest(8 项):
用 MockWebServer.withWebSocketUpgrade 在 JVM 上起**真实** WebSocket 服务端
(能按 §3.2 回 welcome、按 §3.6 回 pong、按指定关闭码关连接),无需设备即可覆盖
「该重连的」与「不该重连的」两条分支。Robolectric 只用来提供 android.os.Build 桩。
它抓到的真实缺陷(本版修复):
上一次会话的关闭码会污染下一次会话,导致重新开始推流后失去自动重连能力。
复现:第一次连接被对端以 1001 关闭(按 §7 不重连,正确)→ 用户再点「开始推流」
→ 这一次是网络层失败(服务端还没起来),按 §7 **应当**重连 → 但 connect() 只重置了
reconnectCounter 与 lastPongAtMs,**没有清 lastCloseCode** → scheduleReconnect()
仍按 1001 判定 → 直接 stopped,**再也不会自动重连**。
测试失败信息(修复前):
✗ 上一次 1001 关闭后重新连接,遇到网络失败仍应按策略重连
AssertionError: 当前 state=STOPPED,lastCloseCode=1001,attempts=0
修复(PoseWebSocketClient.kt):
* connect():清掉会话级关闭状态(lastCloseCode=0、lastCloseReason="");
* onFailure():记录 recordClose(0, …)(契约 §7 中 0=未收到关闭帧 → 应重连),
顺带让界面「最近一次断开原因」显示本次失败而不是旧值。
用例清单:握手→streaming 与 welcome 保活参数解析、按协商间隔发 ping 收 pong、
1011 按退避重连且等待落在策略 ±20% 区间、成功后计数清零、1001/1002 不重连、
主动断开不重连、上一次 1001 后再连遇网络失败仍应重连(本次回归锁)、
服务端未启动时 1.5s 内 ≤3 次尝试(退避生效)。
实测:./gradlew :app:testDebugUnitTest → 6 个测试类 / 64 项用例 / 0 失败
(原 56 项);build/verify.sh 全绿(签名、APK 版本元数据 0.2.1/201、native 库体检、
ABI、R8、dex)。版本号 0.2.0/200 → 0.2.1/201。
踩坑:第一版 MockWebServer 夹具的 101 响应没有回显 Sec-WebSocket-Protocol
(RFC 6455 要求),客户端记录的 negotiatedProtocol 为空导致断言失败 —— 那是
**夹具**不合规而非客户端问题,已在夹具里补上。写「真实对端」夹具时要按 RFC/契约
把服务端做对,否则会把夹具缺陷误判成产品缺陷。
351 lines
21 KiB
Markdown
351 lines
21 KiB
Markdown
# opendance-android
|
||
|
||
OpenDance Studio **阶段三**: Android 辅助 App(Kotlin + CameraX + MediaPipe 端侧推理)。
|
||
|
||
用手机摄像头做**端侧**姿态推理,把 33 个骨骼关键点通过 WebSocket 推给 Windows 客户端,
|
||
由后者驱动 VRM 角色并 DTW 实时评分。
|
||
|
||
> **画面不离开设备**。通过网络发送的只有 33 个关键点坐标,不含任何图像数据。
|
||
|
||
## 状态
|
||
|
||
**阶段三已完成**: T-AND-001 -> T-AND-016 全部落地(含重连状态机与**真实 WebSocket 重连链路测试**、MediaPipe 崩溃修复、APK 产物版本元数据校验、native 库体检)。
|
||
|
||
| 验收项 | 结果 |
|
||
|--|--|
|
||
| 单元测试 | OK **64 项全部通过**(JVM,无需设备;含 MockWebServer 真实 WebSocket 的重连链路集成测试) |
|
||
| **跨端金标准** | OK Kotlin 序列化器与 Web 端 TypeScript 实现**逐位一致** |
|
||
| `./gradlew assembleRelease` | OK 生成**已签名** APK(28.6 MB) |
|
||
| 签名校验 | OK `apksigner` 退出码 0, v2 方案,CN=OpenDance Studio Dev |
|
||
| 包信息 | OK `com.opendance.studio` v0.2.1 (versionCode 201), minSdk 29(Android 10+)/ targetSdk 34 |
|
||
| 模型随包分发 | OK `assets/pose_landmarker_lite.task` 5.7 MB(Stored 未压缩) |
|
||
| ABI 覆盖 | OK arm64-v8a + armeabi-v7a(debug 额外含 x86/x86_64) |
|
||
| APK 版本元数据 | OK 直接读产物 `AndroidManifest` 的 `versionName=0.2.1`/`versionCode=201`,与 `project.yaml` 一致(T-AND-013) |
|
||
| native 库体检 | OK ELF 类型匹配双 ABI、`DT_NEEDED` 闭包完整(无缺失依赖)、`PT_LOAD` 页对齐 `0x4000`(16 KB 页兼容)、`zipalign -P 16` 通过(T-AND-015) |
|
||
|
||
## v0.2.1 修订(重连链路的真实 WebSocket 集成测试 + 一处真实缺陷)
|
||
|
||
**缺陷**:上一次会话的关闭码会**污染下一次会话**,导致重新开始推流后失去自动重连能力。
|
||
复现路径:第一次连接被对端以 `1001` 关闭(按契约 §7 **不重连**,正确)→ 用户再次点
|
||
「开始推流」→ 这一次是**网络层失败**(服务端还没起来),按 §7 **应当**重连 →
|
||
但 `connect()` 只重置了重连计数与 pong 时间,**没有清 `lastCloseCode`**,
|
||
`scheduleReconnect()` 仍按 1001 判定 → 直接 `stopped`,**再也不会自动重连**。
|
||
|
||
**修复**:`connect()` 清掉会话级关闭状态(`lastCloseCode = 0`、`lastCloseReason = ""`);
|
||
`onFailure()` 记录 `recordClose(0, …)`(0 = 未收到关闭帧 → 应重连),
|
||
顺带让界面「最近一次断开原因」显示本次失败而不是旧值。
|
||
|
||
**为什么之前没发现**:`ReconnectPolicyTest`(11 项)验的是**纯函数**「给定关闭码怎么退避」,
|
||
而缺陷在**调用点**(关闭码没被正确重置)。新增
|
||
`app/src/test/java/com/opendance/studio/PoseWebSocketReconnectTest.kt`(8 项):
|
||
用 `MockWebServer.withWebSocketUpgrade` 在 JVM 上起**真实** WebSocket 服务端,
|
||
覆盖「该重连的」(1011 / 服务器未启动)与「不该重连的」(1000/1001/1002 / 主动断开)两条分支,
|
||
并断言重连等待取自策略且落在 ±20% 区间、成功后计数清零。
|
||
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
```bash
|
||
export JAVA_HOME=/path/to/jdk17 # AGP 8.7.3 要求
|
||
export ANDROID_HOME=/path/to/android-sdk
|
||
|
||
bash build/gen-keystore.sh # 生成发布签名(首次;不入库,请自行备份)
|
||
bash build/verify.sh # 单元测试 -> 构建 -> 产物校验
|
||
bash build/build-apk.sh all # 仅构建 debug + release
|
||
bash build/package-release.sh # 打发布包
|
||
```
|
||
|
||
详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md),
|
||
第一次上手请看下文的「初次部署(从零跑起来)」一节。
|
||
|
||
## 初次部署(从零跑起来)
|
||
|
||
本节面向**第一次拿到本仓库**、要在真机上把 App 跑起来的人。
|
||
下文出现的地址、路径、口令一律是**占位符或文档示例值**,请替换为你自己的值,
|
||
并确保这些值**不要提交进仓库**。
|
||
|
||
### 1. 前置条件
|
||
|
||
| 项 | 要求 | 依据 |
|
||
|--|--|--|
|
||
| JDK | 17 或更高(AGP 8.7.3 要求;`build/build-apk.sh` 会校验主版本 >= 17) | `gradle.properties` / `build/build-apk.sh` |
|
||
| Gradle | 8.9,仓库自带 wrapper(`./gradlew`),本机无需另装 | `gradle/wrapper/gradle-wrapper.properties` |
|
||
| Android SDK | `compileSdk 34` / `minSdk 29`(Android 10+)/ `targetSdk 34`;需 `platforms;android-34` 与 `build-tools;34.0.0` | `app/build.gradle.kts` |
|
||
| 设备 | **需要真机**(Android 10 及以上、带摄像头);模拟器只作有限调试 | `app/build.gradle.kts` 的 ABI 过滤 |
|
||
|
||
真机与模拟器的差别:
|
||
|
||
- release 包只打包 `arm64-v8a` + `armeabi-v7a`, **无法安装到 x86/x86_64 模拟器**。
|
||
- debug 包额外含 `x86`/`x86_64`,可以装进模拟器,但模拟器相机是虚拟场景,
|
||
CameraX 取到的是合成画面,MediaPipe 的姿态识别效果不能代表真机,
|
||
也不能作为「第一次跑通」的验收依据。
|
||
- 结论:**姿态识别与联调验收请用真机**。
|
||
|
||
权限:运行页需要**相机权限**(运行时授予)。另外 `INTERNET`、`ACCESS_NETWORK_STATE`、
|
||
`ACCESS_WIFI_STATE` 为普通权限,安装时自动授予,用于 WebSocket 推流与局域网扫描。
|
||
|
||
### 2. 获取代码与本地配置
|
||
|
||
```bash
|
||
git clone https://gitea.kasugano.cn/zijunchiang/opendance-android.git
|
||
cd opendance-android
|
||
```
|
||
|
||
`local.properties` 是**本地文件**(已在 `.gitignore` 中,不会入库),
|
||
只需写 Android SDK 路径:
|
||
|
||
```properties
|
||
sdk.dir=<Android SDK 路径>
|
||
```
|
||
|
||
`build/build-apk.sh` 在该文件缺失时会依据 `ANDROID_HOME` / `ANDROID_SDK_ROOT`
|
||
自动生成它;也可以手动创建。**不要提交它**,每台机器路径不同。
|
||
|
||
签名材料同样**不入库**(`.gitignore` 已忽略 `keystore/`、`*.jks`、`*.keystore`、
|
||
`keystore.properties`):
|
||
|
||
- 仓库里**没有私钥**,只有生成脚本与说明。
|
||
- 首次发布前执行 `bash build/gen-keystore.sh`,生成
|
||
`keystore/opendance-release.jks` 与 `keystore.properties`
|
||
(含 `storeFile`、`keyAlias` 与两个口令字段)。**请自行备份,且不要提交**。
|
||
- `app/build.gradle.kts` 的签名取值优先级:
|
||
`keystore.properties` -> 环境变量(`KEYSTORE_PATH` / `KEYSTORE_PASSWORD` /
|
||
`KEY_ALIAS` / `KEY_PASSWORD`)-> 回退 debug 签名。
|
||
回退只为让 CI 也能产出已签名 APK;正式发布必须提供自己的 keystore。
|
||
|
||
### 3. 首次构建
|
||
|
||
```bash
|
||
export JAVA_HOME=<JDK 17 路径>
|
||
export ANDROID_HOME=<Android SDK 路径>
|
||
|
||
# 方式一:仓库脚本(推荐,构建后自动校验产物)
|
||
bash build/build-apk.sh debug # 仅 debug
|
||
bash build/build-apk.sh release # 仅 release
|
||
bash build/build-apk.sh all # 两者都构建
|
||
|
||
# 方式二:直接调用 Gradle
|
||
./gradlew assembleDebug
|
||
./gradlew assembleRelease
|
||
```
|
||
|
||
产物路径与文件名(固定):
|
||
|
||
| 变体 | 产物 | 说明 |
|
||
|--|--|--|
|
||
| debug | `app/build/outputs/apk/debug/app-debug.apk` | applicationId 带 `.debug` 后缀,含 x86/x86_64 |
|
||
| release | `app/build/outputs/apk/release/app-release.apk` | 仅 arm64-v8a + armeabi-v7a,开启 R8 |
|
||
|
||
发布整包(APK + `THIRD_PARTY.md` + `README.txt` + `contracts/`)用
|
||
`bash build/package-release.sh`,输出在 `dist-release/`。
|
||
|
||
### 4. 安装到手机
|
||
|
||
```bash
|
||
# 用 adb(手机需开启 USB 调试)
|
||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||
adb install -r app/build/outputs/apk/release/app-release.apk
|
||
```
|
||
|
||
也可以直接把 APK 拷到手机,在系统设置中允许「安装未知来源应用」后点击安装。
|
||
|
||
首次进入运行页会弹出**相机权限**请求;拒绝后运行页显示
|
||
「需要相机权限才能进行姿态识别」,可点「授予相机权限」重新申请,
|
||
或到系统「设置 -> 应用 -> OpenDance -> 权限」中手动开启。
|
||
|
||
### 5. 配置与联通(填 Windows 客户端地址)
|
||
|
||
本 App **只连接 Windows 客户端,不访问 Web 服务端**:地址由
|
||
`AppSettings.webSocketUrl()` 拼成 `ws://<host>:<port>/pose`,子协议 `opendance.pose.v1`。
|
||
|
||
连接页字段(对应 `data/AppSettings.kt`):
|
||
|
||
| 界面字段 | 设置项 | 默认值 | 说明 |
|
||
|--|--|--|--|
|
||
| IP 地址 | `host` | RFC 5737 文档网段占位地址(源码为 `192.0.2.100`, **非真实主机**) | 填 Windows 电脑的局域网 IP |
|
||
| 端口 | `port` | `8765` | 与 Windows 客户端监听端口一致 |
|
||
| 目标帧率(FPS) | `targetFps` | `30` | 取值范围 5~60 |
|
||
| 分辨率 | `resolution` | `640x480` | 可选 `480x360` / `640x480` / `1280x720` |
|
||
| 镜像画面 | `mirror` | 开 | 只影响显示与 `flags.bit0`,发送的是未镜像坐标 |
|
||
| 仅上半身模式 | `upperBodyOnly` | 关 | 下半身置信度置 0,接收端插值 |
|
||
|
||
填写示例(占位符,请替换):
|
||
|
||
```
|
||
IP 地址:192.0.2.10 # RFC 5737 文档示例网段,实际填你电脑的局域网 IP
|
||
端口: 8765
|
||
```
|
||
|
||
不想手填 IP 时,可点「扫描局域网」:App 取本机 WiFi IPv4 地址后推导 `/24` 网段,
|
||
以 32 并发、单机 300ms 超时探测该网段内开放 8765 端口的主机(约 2~3 秒),
|
||
结果按延迟升序展示,点候选即可填入。也可以先点「测试连接」确认端口可达。
|
||
|
||
### 6. 第一次跑通
|
||
|
||
数据流是**单向**的:`手机 -> Windows 客户端`;手机端不连 Web 服务端。
|
||
|
||
1. 手机与 Windows 电脑连到**同一个局域网 / WiFi**(路由器不要开 AP 隔离)。
|
||
2. 在电脑上启动 OpenDance Studio Windows 客户端,确认它在监听 8765 端口。
|
||
3. 手机打开 App,连接页填入电脑 IP(或用「扫描局域网」自动发现),点「测试连接」。
|
||
4. 点「开始推流」,首次会申请相机权限,允许后进入运行页。
|
||
5. 站到画面中央,屏幕上出现骨架叠加。
|
||
|
||
成功标志:
|
||
|
||
| 位置 | 可观察现象 |
|
||
|--|--|
|
||
| App 运行页 | 「连接状态」为**推流中**(`streaming`;短暂经过 解析地址 → 连接中 → 握手中) |
|
||
| App 运行页 | 「目标」显示 `ws://<IP>:<端口>/pose`;服务端返回 welcome 后出现「会话」ID 与「协商子协议」 |
|
||
| App 运行页 | 「已发送」帧数持续增长;「发送帧率」「识别帧率」大于 0;「推理耗时」为毫秒数 |
|
||
| App 运行页 | 「保活」显示心跳与静默超时(由服务端在 welcome 中声明),心跳必须小于静默超时的一半 |
|
||
| App 运行页 | 断开时「最近断开」显示 close code 与原因(如 `1001 对端离开`),而不是只有"未连接" |
|
||
| App 运行页 | 「人体识别」为**已识别**,叠加层出现骨架(黄点 = 置信度好,红点 = 置信度低) |
|
||
| Windows 客户端 | 显示已连接并收到关键点,VRM 模型随动作实时动起来 |
|
||
|
||
### 7. 首次部署自检清单
|
||
|
||
| # | 命令 | 预期输出 |
|
||
|--|--|--|
|
||
| 1 | `./gradlew testDebugUnitTest` | `BUILD SUCCESSFUL`,全部用例通过;报告在 `app/build/reports/tests/testDebugUnitTest/index.html` |
|
||
| 2 | `bash build/build-apk.sh debug` | 末尾打印 `OK Debug APK: app/build/outputs/apk/debug/app-debug.apk` 及其大小与签名信息 |
|
||
| 3 | `bash build/build-apk.sh all` | debug 与 release 均 `OK`; release 显示 `已签名(APK Signature Scheme v2)` |
|
||
| 4 | `$ANDROID_HOME/build-tools/34.0.0/apksigner verify app/build/outputs/apk/release/app-release.apk` | 退出码 0(无输出即通过) |
|
||
| 5 | `unzip -l app/build/outputs/apk/release/app-release.apk \| grep pose_landmarker_lite.task` | 列出 `assets/pose_landmarker_lite.task`(约 5.7 MB, Stored 未压缩) |
|
||
| 6 | `./gradlew verifyReleaseAbi` | release APK 的 ABI 恰为 `[arm64-v8a, armeabi-v7a]`,且每个 ABI 下都有 MediaPipe native 库 |
|
||
| 7 | `grep -c "^com.google.mediapipe" app/build/outputs/mapping/release/seeds.txt` | 大于 0(说明 R8 没有裁掉 MediaPipe;为 0 则 release 必崩) |
|
||
| 8 | `adb install -r app/build/outputs/apk/debug/app-debug.apk` | `Success` |
|
||
|
||
其中 `KeypointFrameTest.goldenVectorMatchesWebImplementation` 必须通过:
|
||
它断言本端 Kotlin 序列化器与 Web 端 TypeScript 实现产出**逐位相同**的 239 字节帧。
|
||
|
||
### 8. 常见问题(首次部署)
|
||
|
||
| 现象 | 可能原因 | 处理 |
|
||
|--|--|--|
|
||
| 相机黑屏,或一直停在「需要相机权限才能进行姿态识别」 | 相机权限被拒绝或已被永久拒绝;也可能相机被其他 App 占用 | 在运行页点「授予相机权限」;若不再弹窗,去系统「设置 -> 应用 -> OpenDance -> 权限」手动开启,并关闭占用相机的其它 App |
|
||
| 相机预览正常但「人体识别」一直是「未识别到人体」 | 光线不足、人不在画面内、或分辨率/模式不合适 | 站到画面中央并改善光照;确认分辨率不是过低;观察「推理耗时」是否异常大 |
|
||
| 「连接状态」一直「已断开 / 重连中」,「测试连接」失败 | IP 或端口填错;Windows 客户端未启动;防火墙拦截入站 8765;手机与电脑不在同一网段(AP 隔离) | 核对连接页「本机地址」与电脑 IP 是否同网段;启动客户端;在 Windows 防火墙放行 OpenDance 的 TCP 8765 入站;关闭路由器 AP 隔离 |
|
||
| 「扫描局域网」扫不到主机 | 手机未连 WiFi、路由器 AP 隔离、或客户端未监听 8765 | 确认手机连的是 WiFi 而非蜂窝;改为手动填 IP;检查客户端监听端口与防火墙 |
|
||
| 点「开始推流」立即闪退,或提示 MediaPipe 初始化失败 | MediaPipe 原生库加载 / 静态初始化失败抛出的是 **Error** 子类(`UnsatisfiedLinkError`、`ExceptionInInitializerError`),不是 `Exception`;常见诱因是 ABI 不匹配(例如把只含 arm 的 release 包装到 x86 模拟器)、`extractNativeLibs=false`、R8 裁掉 MediaPipe 或模型资源缺失 | **先看 App 给出的诊断**:`PoseDiagnostics` 会把 cause 链最内层的真因、设备 ABI、APK 内 ABI 与结论一起显示在运行页(例如"APK 只包含 arm64-v8a/armeabi-v7a,而模拟器(x86_64)需要 x86_64")。真机用 release、模拟器调试用 debug;仓库已用 `jniLibs.useLegacyPackaging = true` 处理 native 解压,并有 `-keepclasseswithmembernames` + `-printseeds` 防止 R8 裁掉 MediaPipe |
|
||
| 画面里没有人 / 相机暂停一段时间后连接被断开 | 这是**静默超时**(服务端在 welcome 里声明 `silentTimeoutMs`),即使没有关键点,App 也会按 `heartbeatIntervalMs` 发应用层 `ping` | 正常情况下不会触发。若仍频繁断开,运行页「保活」一行会显示两端的参数;契约要求心跳小于静默超时的一半,可在 Windows 端 `pose_server.gd` 调整并在两端核对日志 |
|
||
| 断网恢复后不自动重连 | 契约 §7 规定:**只有真正断开才重连**;服务端主动关闭(`1000`/`1001`)或协议错误(`1002`)以及用户主动停止都**不**重连,避免"重连风暴" | 运行页「最近断开」会写明 close code 与原因。若原因是 `1001 对端离开`,说明 Windows 客户端那边重启或主动关闭了推流,需要在电脑端恢复后再点一次「开始推流」 |
|
||
| Windows 端「已连接 N 台」忽大忽小 | 同一次闪断被计成多台设备 | 契约 §3.1 要求 hello 携带稳定的 `deviceId`(本端取 ANDROID_ID 的哈希,不含硬件序列号),接收端按它去重。核对两端日志中的 deviceId 是否一致 |
|
||
|
||
## 技术栈
|
||
|
||
| 组件 | 选型 |
|
||
|--|--|
|
||
| 语言 | Kotlin 2.0.21 |
|
||
| 构建 | AGP 8.7.3 + Gradle 8.9 + JDK 17 |
|
||
| 相机 | CameraX 1.4.0 |
|
||
| 推理 | MediaPipe Tasks Vision 0.10.35 |
|
||
| 通信 | OkHttp 4.12.0 |
|
||
| UI | Jetpack Compose(BOM 2024.10.01, Material3) |
|
||
|
||
## 数据流
|
||
|
||
```
|
||
CameraX(ImageAnalysis) ──▶ MediaPipe PoseLandmarker(端侧推理)
|
||
│ 33 点归一化坐标
|
||
┌─────────────────────┴─────────────────────┐
|
||
▼ ▼
|
||
KeypointFrame(239B) ──▶ WebSocket ──▶ Windows 骨架叠加层
|
||
```
|
||
|
||
```
|
||
app/src/main/java/com/opendance/studio/
|
||
├── model/ 239 字节帧编解码 + 33 点拓扑(契约核心)
|
||
├── camera/ CameraX 采集
|
||
├── pose/ MediaPipe 端侧推理
|
||
├── net/ WebSocket 推流 + 局域网主机发现
|
||
├── data/ 设置持久化
|
||
└── ui/ 连接页 / 运行页 / 关于页
|
||
```
|
||
|
||
## 依赖的契约(opendance-web 冻结,逐项独立版本)
|
||
|
||
| 契约 | 版本 |
|
||
|--|--|
|
||
| `websocket-keypoints` | 0.2.0(本端为发送方) |
|
||
| `skeleton-topology` | 0.2.0 |
|
||
| `rest-api` | 0.5.0 |
|
||
| `data-models` | 0.7.0 |
|
||
| `model-formats` | 0.5.0 |
|
||
| `database` | 0.4.0 |
|
||
|
||
版本号来源统一(文档 §11.2):应用版本取 `app/build.gradle.kts` 的 `versionName`
|
||
(= `project.yaml` 的 `version` = APK 文件名 = Git tag),契约版本在构建期从
|
||
`contracts/VERSION` 解析后注入 `BuildConfig`;Kotlin 界面代码里不出现任何版本号字面量。
|
||
|
||
**native 库体检(T-AND-015)**:`ExceptionInInitializerError` 这类「一装上就崩」的
|
||
问题大多出在 native 库打包上,而这几项**无设备也能查**:
|
||
|
||
```bash
|
||
python3 build/check_native_libs.py dist-release/opendance-android-v0.2.1/app-release.apk
|
||
# -> 每个 ABI 下每个 .so 的 ELF 类型 / DT_NEEDED 闭包 / PT_LOAD 页对齐
|
||
# -> ✓ native 库体检通过:arm64-v8a, armeabi-v7a | ELF 类型匹配、依赖闭包完整、16 KB 页对齐
|
||
```
|
||
|
||
纯 Python 标准库实现(不需要 readelf / NDK),已接入 `verify.sh` 与 `accept-release.sh`。
|
||
与 `readelf` 在 6 个 `.so` 上逐项交叉验证一致;三条检测分支都用真实负例反向验证过
|
||
(错 ABI 的 .so、缺失依赖、页对齐不足)。**它不替代真机**,只把「打包配置错误」挡在发布前。
|
||
|
||
**发布包级验收(T-AND-014)**:`build/verify.sh` 验的是本地刚构建的 APK,
|
||
而用户装的是 zip 里的 `app-release.apk`。发布前对包再跑一次:
|
||
|
||
```bash
|
||
bash build/accept-release.sh dist-release/opendance-android-v0.2.1.zip
|
||
# -> sha256 / 文件清单 / AndroidManifest versionName+versionCode / ABI+MediaPipe native
|
||
# / 模型资源 / dex 中的 PoseLandmarker / 验签 / 包内契约快照与仓库一致
|
||
# -> 发布包级验收全部通过
|
||
```
|
||
|
||
已对**已发布的 v0.2.0 资产**(重新下载)跑通,并用「篡改包内 `contracts/VERSION`」
|
||
与「期望版本写错」两个负例反向验证过。
|
||
|
||
**产物侧复核(T-AND-013)**:`versionName` 是**构建配置**,而安装界面 / 应用信息页 /
|
||
`dumpsys package` 读的是 **APK 的 `AndroidManifest`**。构建缓存、复用旧 APK、
|
||
打包时拷错文件都会造成「配置全对、产物是旧版」,配置层校验抓不到(**Windows 客户端**
|
||
的 v0.2.1 就踩过同类坑:配置文件全一致,exe 属性页显示 0.1.0)。因此新增:
|
||
|
||
```bash
|
||
bash build/check-apk-version.sh app/build/outputs/apk/release/app-release.apk
|
||
# -> AndroidManifest versionName = 0.2.1 / versionCode = 201
|
||
# -> ✓ APK 版本元数据一致:0.2.1(versionCode 201)
|
||
```
|
||
|
||
`build/check_apk_version.py` 直接解析二进制 AXML,**不需要 Android SDK / aapt2**
|
||
(已与 `aapt2 dump badging` 在 v0.1.0 / v0.2.0 两个真实 APK 上交叉验证逐项一致)。
|
||
该检查已接入 `build/verify.sh`(步骤 3/3)与 `build/package-release.sh`(打包前 `die`)。
|
||
|
||
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧(本端为**发送方**)
|
||
- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) - MediaPipe 33 点索引与坐标语义
|
||
- [`contracts/open-source-credits.md`](contracts/open-source-credits.md) - 关于页必须展示的开源声明
|
||
|
||
`contracts/` 是**只读快照**。任何跨端契约变更必须先回到
|
||
[`opendance-web`](https://gitea.kasugano.cn/zijunchiang/opendance-web) 修改并提升版本。
|
||
|
||
## 跨端一致性:金标准字节向量测试
|
||
|
||
本仓库最重要的测试不是「函数跑通了」,而是**与另两端逐位一致**:
|
||
|
||
`KeypointFrameTest.goldenVectorMatchesWebImplementation` 用 opendance-web 已通过验收的
|
||
TypeScript 实现生成 239 字节金标准向量,断言 Kotlin 实现产出完全相同的字节。
|
||
这条测试同时守护三端(TypeScript / GDScript / Kotlin),任何一端私自改字段布局都会立刻失败。
|
||
|
||
## 已知限制
|
||
|
||
1. Godot(Windows 端)的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
|
||
本端仍按契约连接到 `/pose`,互操作不受影响。
|
||
2. **未在真实 Android 设备上安装运行**(本环境无设备也无模拟器)。
|
||
已验证到:构建通过、签名有效、包信息正确、模型与 4 个 ABI 的 native 库均正确打包。
|
||
3. 主机发现依赖手机与电脑处于同一 WiFi;路由器开启 AP 隔离时无法互访(见 README.txt FAQ)。
|
||
4. release 包只含 arm64-v8a + armeabi-v7a。x86/x86_64 仅供模拟器,会占掉约 27 MB,
|
||
对真机用户没有价值;debug 包保留全部 4 个 ABI 以便模拟器调试。
|
||
|
||
## 第三方开源声明
|
||
|
||
见 [`THIRD_PARTY.md`](THIRD_PARTY.md),或应用内「关于」页面。
|
||
未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。
|