docs(android): 任务说明、DSH 约束与交付文档
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# T-AND-001 Android Studio 项目骨架
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:无
|
||||
|
||||
## 目标
|
||||
|
||||
建立 Kotlin + Compose 的 Android 工程骨架,配置三页导航与共享设置。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
settings.gradle.kts
|
||||
build.gradle.kts
|
||||
app/build.gradle.kts
|
||||
app/src/main/AndroidManifest.xml
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `settings.gradle.kts` / `build.gradle.kts` | AGP 8.7.3 + Kotlin 2.0.21 + Compose 插件 |
|
||||
| `gradle/wrapper/` | Gradle 8.9 wrapper(`./gradlew` 可直接用) |
|
||||
| `app/build.gradle.kts` | minSdk 29 / targetSdk 34;R8、签名、ABI、模型不压缩 |
|
||||
| `app/src/main/AndroidManifest.xml` | 相机 + 网络权限;`usesCleartextTraffic`(局域网明文 WS) |
|
||||
| `MainActivity.kt` | 三页状态机导航(连接 / 运行 / 关于) |
|
||||
| `OpenDanceApp.kt` | Application 单例容器 |
|
||||
| `data/AppSettings.kt` | SharedPreferences 设置 |
|
||||
| `ui/theme/Theme.kt` | 与另两端一致的深色配色 |
|
||||
|
||||
## 版本组合(均已验证可构建)
|
||||
|
||||
```
|
||||
JDK 17 / Gradle 8.9 / AGP 8.7.3 / Kotlin 2.0.21 / Compose BOM 2024.10.01
|
||||
```
|
||||
|
||||
## 验收
|
||||
|
||||
```bash
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
## 结果
|
||||
|
||||
- `assembleDebug` 成功;`MainActivity` 三页导航可用。
|
||||
@@ -0,0 +1,39 @@
|
||||
# T-AND-002 CameraX 采集
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-001
|
||||
|
||||
## 目标
|
||||
|
||||
用 CameraX 采集相机画面,为 MediaPipe 提供连续帧。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
app/src/main/java/com/opendance/studio/camera/CameraController.kt
|
||||
app/src/main/java/com/opendance/studio/data/AppSettings.kt
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
`camera/CameraController.kt`:Preview + ImageAnalysis 绑定、分辨率档位、前后摄切换。
|
||||
|
||||
## 关键实现
|
||||
|
||||
| 项 | 取值/做法 | 理由 |
|
||||
|---|---|---|
|
||||
| 背压策略 | `STRATEGY_KEEP_ONLY_LATEST` | 推理慢时自动丢帧,预览不卡顿 |
|
||||
| 输出格式 | `OUTPUT_IMAGE_FORMAT_RGBA_8888` | MediaPipe 需要 Bitmap,避免 YUV 转换 |
|
||||
| 分辨率 | 三档(480×360 / 640×480 / 1280×720),`FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER` | 低端机可降档保帧率 |
|
||||
| 分析线程 | 单线程 Executor | 推理串行,避免并发抢占 |
|
||||
| 镜像 | 预览镜像由 PreviewView 处理;**发送原始未镜像坐标** | 契约要求由 flags.bit0 声明镜像语义 |
|
||||
|
||||
## 隐私
|
||||
|
||||
相机帧只在内存中流转到推理器,**不落盘、不上传**。离开运行页即 `unbindAll()`,
|
||||
不在后台占用相机。
|
||||
|
||||
## 验收
|
||||
|
||||
`assembleDebug` 通过;运行页可正常预览(需真机,见 tasks/009 的环境说明)。
|
||||
@@ -0,0 +1,48 @@
|
||||
# T-AND-003 MediaPipe Pose 端侧推理
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-002
|
||||
|
||||
## 目标
|
||||
|
||||
用 MediaPipe Tasks Vision 在设备本地做姿态推理,输出 33 个归一化关键点。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
contracts/skeleton-topology.md # 锚点:§1 33 点、§4 坐标系
|
||||
app/src/main/java/com/opendance/studio/pose/PoseDetector.kt
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
`pose/PoseDetector.kt` + `app/src/main/assets/pose_landmarker_lite.task`(5.7 MB)。
|
||||
|
||||
## 关键实现
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 运行模式 | `LIVE_STREAM` + `detectAsync`,不阻塞相机线程 |
|
||||
| 模型 | `pose_landmarker_lite.task`(随 APK 分发,`noCompress "task"` 保证可读) |
|
||||
| 置信度阈值 | detection / presence / tracking 均 0.5 |
|
||||
| 背压 | `busy` 标志:上一帧仍在推理时直接丢弃新帧并计数,不排队 |
|
||||
| 时间戳 | 单调递增校正(MediaPipe 要求严格递增) |
|
||||
| 缺失点 | 置信度置 0、坐标归零,接收端按契约插值 |
|
||||
|
||||
## 坐标与镜像语义
|
||||
|
||||
- 发送**未镜像的原始 MediaPipe 坐标**(契约 §4)
|
||||
- 是否镜像由帧头 `flags.bit0` 声明,接收端据此决定如何还原
|
||||
- 叠加层绘制时才取 `1 - x`,与前置摄像头预览的镜像观感一致
|
||||
|
||||
这保证了同一份关键点数据在「镜像预览」与「真实左右」两种解释下都不会歧义。
|
||||
|
||||
## 上半身模式
|
||||
|
||||
`upperBodyOnly = true` 时,髋部及以下点置信度置 0 并置 `flags.bit2`,
|
||||
接收端会插值。用于带宽/算力不足时降级。
|
||||
|
||||
## 验收
|
||||
|
||||
`assembleDebug` 通过;运行页在真机上可看到骨架叠加层(见 tasks/009)。
|
||||
@@ -0,0 +1,74 @@
|
||||
# T-AND-004 33 点 → 239 字节帧
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-003(数据结构)、T-AND-001
|
||||
|
||||
## 目标
|
||||
|
||||
把 33 点关键点按冻结契约序列化为 239 字节二进制帧。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
contracts/websocket-keypoints.md # 锚点:§2 二进制帧(冻结)
|
||||
contracts/skeleton-topology.md # 锚点:§1 33 点、§4 坐标系
|
||||
app/src/main/java/com/opendance/studio/model/KeypointFrame.kt
|
||||
app/src/main/java/com/opendance/studio/model/SkeletonTopology.kt
|
||||
```
|
||||
|
||||
## 帧布局(契约冻结)
|
||||
|
||||
```
|
||||
偏移 长度 字段
|
||||
0 2 magic u16 = 0x4F44 ('OD'),小端序 → 字节序为 44 4F
|
||||
2 1 version u8 = 1
|
||||
3 1 flags u8 bit0 镜像 | bit1 低置信度 | bit2 上半身
|
||||
4 4 frame_id u32 LE
|
||||
8 231 keypoints 33 × 7B:s16 x, s16 y, s16 z, u8 conf
|
||||
合计 239
|
||||
```
|
||||
|
||||
## ★ 金标准字节向量测试
|
||||
|
||||
本任务的核心验收不是「函数跑通了」,而是**与另两端逐位一致**。
|
||||
|
||||
测试 `KeypointFrameTest.goldenVectorMatchesWebImplementation` 使用由
|
||||
`opendance-web` 已通过验收的 TypeScript 实现产出的 239 字节金标准向量:
|
||||
|
||||
```bash
|
||||
# 重新生成金标准向量(在 opendance-web 仓库内)
|
||||
cd opendance-web/web-server && npm run build
|
||||
node -e "
|
||||
const { encodeBinaryFrame } = require('./dist/preprocess/keypoint-codec.js');
|
||||
const points = [], conf = [];
|
||||
for (let i = 0; i < 33; i++) {
|
||||
points.push([(i%33)/32, 1-(i%33)/32, ((i%7)-3)/10]);
|
||||
conf.push(i%2===0 ? 1.0 : 0.5);
|
||||
}
|
||||
const buf = encodeBinaryFrame(42, points.map((p,i)=>[...p,conf[i]]), 0b101);
|
||||
console.log(buf.toString('hex'));
|
||||
"
|
||||
```
|
||||
|
||||
固定输入:`xs = (i%33)/32`、`ys = 1-(i%33)/32`、`zs = ((i%7)-3)/10`、
|
||||
`confidence = i%2==0 ? 1.0 : 0.5`、`frameId = 42`、`flags = 0b101`。
|
||||
|
||||
## 关键实现
|
||||
|
||||
- `ByteBuffer.order(LITTLE_ENDIAN)`,避免手动移位出错
|
||||
- 定点换算 `round(v × 32767)` 并 `coerceIn(-32768, 32767)`
|
||||
- 置信度 `round(v × 255)`,阈值 0.5 含端点(`>=`)
|
||||
- 输入超范围时裁剪而非抛异常(相机抖动可能产生轻微越界值)
|
||||
- 并行 FloatArray 存储,30 FPS 循环内零对象分配
|
||||
|
||||
## 验收
|
||||
|
||||
```bash
|
||||
./gradlew testDebugUnitTest
|
||||
```
|
||||
|
||||
19 项单元测试全部通过,含:
|
||||
常量与契约一致、金标准向量逐位一致、编解码往返精度、错误长度/magic/版本拒绝、
|
||||
极值不溢出、超范围裁剪、低置信度标志计算、镜像 x 还原、拓扑父链表无环、
|
||||
骨架连线索引合法。
|
||||
@@ -0,0 +1,47 @@
|
||||
# T-AND-005 WebSocket 推流 + 自动重连
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-004
|
||||
|
||||
## 目标
|
||||
|
||||
把 239 字节帧与控制消息推给 Windows 客户端,网络抖动时自动恢复。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
contracts/websocket-keypoints.md # 锚点:§1 端点与子协议、§3 控制帧
|
||||
app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
`net/PoseWebSocketClient.kt`:OkHttp WebSocket 封装 + 状态流 + 统计。
|
||||
|
||||
## 关键实现
|
||||
|
||||
| 项 | 做法 | 理由 |
|
||||
|---|---|---|
|
||||
| 子协议 | 请求头 `Sec-WebSocket-Protocol: opendance.pose.v1` | 契约 §1 |
|
||||
| 二进制帧 | `socket.send(ByteString)` | 契约要求生产链路用二进制 |
|
||||
| 控制帧 | `send(text)`,JSON | hello / stats / bye |
|
||||
| 自动重连 | 指数退避 1s → 2s → 4s … 上限 15s,连接成功即重置 | 避免抖动时打爆对端 |
|
||||
| 非连接态发送 | 直接丢弃并计入 `droppedFrames` | **绝不阻塞相机线程** |
|
||||
| 心跳 | OkHttp `pingInterval(15s)` | 及时发现半开连接 |
|
||||
| 读超时 | 0(不超时) | WebSocket 长连接 |
|
||||
| 统计 | 发送帧数、丢弃帧数、发送 FPS、RTT、重连次数、最近错误 | 运行页状态指示 |
|
||||
|
||||
## 控制帧
|
||||
|
||||
- **hello**(连接后立即发):protocol / device / appVersion / cameraFacing /
|
||||
targetFps / resolution
|
||||
- **welcome**(收):记录服务端 sessionId
|
||||
- **config**(收):服务端可下发 mirror / targetFps / upperBodyOnly / resolution,
|
||||
通过 `onRemoteConfig` 回调
|
||||
- **stats**(每秒发):fps / dropped / latencyMs / confidenceAvg
|
||||
- **bye**(主动断开时发)
|
||||
|
||||
## 验收
|
||||
|
||||
`assembleDebug` 通过;状态机与统计逻辑由运行页展示(需真机验证端到端)。
|
||||
@@ -0,0 +1,47 @@
|
||||
# T-AND-006 连接页 + 主机发现
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-005
|
||||
|
||||
## 目标
|
||||
|
||||
让用户快速找到并连上 Windows 客户端,无需手输 IP。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
app/src/main/java/com/opendance/studio/ui/ConnectScreen.kt
|
||||
app/src/main/java/com/opendance/studio/net/HostDiscovery.kt
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
- `ui/ConnectScreen.kt`:地址输入、扫描、测试连接、推流参数
|
||||
- `net/HostDiscovery.kt`:局域网扫描
|
||||
|
||||
## 主机发现策略
|
||||
|
||||
Windows 客户端是**裸 WebSocket 服务端**(不是 HTTP 服务),无法靠扫描
|
||||
HTTP 端点发现。因此:
|
||||
|
||||
1. 取手机在 WiFi 网的 IPv4 地址(排除 `rmnet*` 蜂窝网卡与 `169.254.*` 链路本地地址)
|
||||
2. 推导 `/24` 网段
|
||||
3. 并发 TCP 探测 `192.168.x.1-254:8765`(并发 32、单机超时 300ms)
|
||||
4. 以 `Flow` 形式边扫边产出,按延迟升序展示——最快响应的最可能是目标机
|
||||
|
||||
整段扫描约 2~3 秒。
|
||||
|
||||
## 可配置项
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| IP / 端口 | 默认 8765,与契约一致 |
|
||||
| 分辨率 | 480×360 / 640×480 / 1280×720 |
|
||||
| 目标帧率 | 写入 hello 的 targetFps |
|
||||
| 镜像画面 | 写入 flags.bit0 |
|
||||
| 仅上半身模式 | 写入 flags.bit2 |
|
||||
|
||||
## 验收
|
||||
|
||||
`assembleDebug` 通过;连接页可编辑参数并持久化(SharedPreferences)。
|
||||
@@ -0,0 +1,54 @@
|
||||
# T-AND-007 运行页 + 状态指示
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-003 / 005 / 006
|
||||
|
||||
## 目标
|
||||
|
||||
预览 + 骨架叠加 + 实时状态,让用户明确知道「现在到底通不通、识别得准不准」。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
app/src/main/java/com/opendance/studio/ui/RunningScreen.kt
|
||||
app/src/main/java/com/opendance/studio/ui/SkeletonOverlay.kt
|
||||
```
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
CameraX(ImageAnalysis) ──ImageProxy──▶ MediaPipe PoseLandmarker(端侧)
|
||||
│ 33 点
|
||||
┌───────────────────────┴───────────────────────┐
|
||||
▼ ▼
|
||||
KeypointFrame(239B) ──▶ WebSocket ──▶ Windows 骨架叠加层
|
||||
```
|
||||
|
||||
## 状态指示(本任务的验收重点)
|
||||
|
||||
| 指标 | 含义 |
|
||||
|---|---|
|
||||
| 连接状态 | 未连接 / 连接中 / 已连接 / 重连中(第 N 次)/ 已断开 |
|
||||
| 目标 | 完整 WS 地址 |
|
||||
| 会话 | 服务端 welcome 回来的 sessionId |
|
||||
| 已发送 / 已丢弃 | 帧数;丢弃 > 0 时提示「推理/连接跟不上」 |
|
||||
| 发送帧率 / 识别帧率 | 端到端吞吐的两个关键指标 |
|
||||
| 推理耗时 | 单帧 MediaPipe 推理毫秒数 |
|
||||
| 人体识别 | 已识别 / 未识别到人体(醒目黄色提示) |
|
||||
| 平均置信度 | < 70% 时转黄,提示光线或遮挡问题 |
|
||||
| 最近错误 | 连接或推理的错误原因 |
|
||||
|
||||
叠加层把 **低置信度点画成红色**,用户能直接看出是哪个部位没识别到——
|
||||
比只显示一个错误码有用得多。
|
||||
|
||||
## 其他
|
||||
|
||||
- 相机权限未授予时显示说明 + 授权按钮,并强调「画面只在本地推理」
|
||||
- 支持前后摄像头切换(复用同一个分析器实例)
|
||||
- 返回键在运行页等同于结束推流,避免相机后台占用
|
||||
|
||||
## 验收
|
||||
|
||||
`assembleDebug` 通过;页面构造路径在构建期完成类型检查。
|
||||
端到端推流需真机,见 tasks/009 的环境说明。
|
||||
@@ -0,0 +1,36 @@
|
||||
# T-AND-008 关于页 + 开源声明
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-006
|
||||
|
||||
## 目标
|
||||
|
||||
展示版本、隐私说明与完整可滚动的第三方开源声明。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
contracts/open-source-credits.md # 锚点:全表
|
||||
app/src/main/java/com/opendance/studio/ui/AboutScreen.kt
|
||||
app/src/main/java/com/opendance/studio/ui/Credits.kt
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
- `ui/Credits.kt`:15 项声明数据(与契约表逐条对应)
|
||||
- `ui/AboutScreen.kt`:版本卡片 + 隐私说明 + 可滚动声明列表
|
||||
|
||||
## 内容要点
|
||||
|
||||
- 版本、契约版本、WebSocket 子协议、关键点拓扑、传输帧长
|
||||
- **隐私说明**:明确写出「画面不会离开手机」「只发送 33 个关键点坐标」
|
||||
- 明确声明不使用 SMPL 拓扑
|
||||
- 每条含许可证、版权(如有)、用途、可点击源链接
|
||||
|
||||
## 验收
|
||||
|
||||
`CreditsTest`(JVM 单元测试)断言:
|
||||
条目数 ≥ 10、文档要求的 10 项全部在列、每条有许可证与 https 链接、
|
||||
已知项目的许可证正确、本端自身依赖(MediaPipe/OkHttp/CameraX/Compose)均在列、
|
||||
项目名不重复、契约版本为 0.1.0。
|
||||
@@ -0,0 +1,74 @@
|
||||
# T-AND-009 APK 打包
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-001 ~ 008
|
||||
|
||||
## 目标
|
||||
|
||||
产出可安装的签名 APK,并提供命令行与 Android Studio 两条路径。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
build/build-apk.sh
|
||||
build/gen-keystore.sh
|
||||
build/verify.sh
|
||||
app/build.gradle.kts
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `build/gen-keystore.sh` | 生成发布签名密钥库(**不入库**) |
|
||||
| `build/build-apk.sh` | 构建 + 校验产物(魔数、签名方案) |
|
||||
| `build/verify.sh` | 单元测试 → 构建 → 产物校验 一条龙 |
|
||||
|
||||
## 签名设计
|
||||
|
||||
`app/build.gradle.kts` 按优先级读取签名信息:
|
||||
|
||||
```
|
||||
1. keystore.properties(build/gen-keystore.sh 生成,已 gitignore)
|
||||
2. 环境变量 KEYSTORE_PATH / KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD
|
||||
3. 两者都缺失 → 回退 debug 签名,保证 CI 上 assembleRelease 也能产出已签名 APK
|
||||
```
|
||||
|
||||
正式发布必须走 1 或 2;回退路径仅为让 CI/验收可跑通,并在构建日志中给出提示。
|
||||
|
||||
## Android Studio 图形界面路径
|
||||
|
||||
```
|
||||
1. Android Studio 打开 opendance-android/
|
||||
2. Build → Generate Signed Bundle / APK
|
||||
3. 选择 APK → 选择或创建 keystore → 选择 release
|
||||
4. 生成 app-release.apk
|
||||
```
|
||||
|
||||
## 验收(文档 §6.3 T-AND-009)
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|---|---|
|
||||
| Android Studio 可 Build → Generate Signed APK | ✅ 工程为标准 AGP 结构,`keystore.properties` 就位即可 |
|
||||
| `./gradlew assembleRelease` 成功生成签名 APK | ✅ 见下 |
|
||||
| APK 在 Android 10+ 设备可安装运行 | ⚠️ 本环境无 Android 设备/模拟器,未实机安装 |
|
||||
| 关于页包含开源项目声明 | ✅ `CreditsTest` 覆盖,且声明随包分发 |
|
||||
|
||||
## 实测产物
|
||||
|
||||
```
|
||||
app-release.apk 55.5 MiB apksigner: Verifies(v2 方案)
|
||||
Signer #1 DN: CN=OpenDance Studio Dev, OU=OpenDance, O=OpenDance Studio, C=CN
|
||||
package: com.opendance.studio versionName 0.1.0
|
||||
minSdk 29(Android 10) targetSdk 34
|
||||
ABI: arm64-v8a, armeabi-v7a, x86, x86_64
|
||||
assets/pose_landmarker_lite.task 5.7 MB(Stored 未压缩)
|
||||
```
|
||||
|
||||
## 环境记录
|
||||
|
||||
- JDK 17(`/usr/lib/jvm/java-17-openjdk-amd64`)
|
||||
- Android SDK:`platforms;android-34`、`build-tools;34.0.0`
|
||||
- 构建期修复的问题:`proguard-rules.pro` 缺失导致 R8 配置告警(已补全 keep 规则)、
|
||||
androidTest 的 `androidx.test.runner` 依赖补齐
|
||||
Reference in New Issue
Block a user