-
released this
2026-09-18 02:34:59 +08:00 | 1 commits to main since this releaseOpenDance Studio -- Android 辅助 App v0.2.2
修复一处真实缺陷:契约允许「合规但不回
pong」的接收端,而客户端却会把这类
接收端每 15 秒误判为链路已死并强制重连 —— 正是"频繁重连"的症状。
另新增可注入计时,使契约 §7.1 的退避封顶序列能在 JVM 里端到端验证。契约快照不变:
rest-api 0.5.0/websocket-keypoints 0.2.0/data-models 0.7.0/
skeleton-topology 0.2.0/model-formats 0.5.0/database 0.4.0
(与 opendance-web / opendance-windows 逐字节一致)。一、修复的缺陷(⑦ 连接与重连)
契约 §3.6 明确:
接收端收到任何一帧(二进制或文本)都算活动,
ping无需额外应答即可保活;
pong只用于诊断。即"合规但不回
pong"的接收端是合法的。但PoseWebSocketClient.onOpen里写的是
lastPongAtMs = System.currentTimeMillis(),于是心跳循环的守卫
if (lastPongAtMs > 0 && silentFor > limit)恒为真:
对任何接收端都会「判定链路已死 → 主动断链 → 重连」,默认参数
(heartbeatIntervalMs = 5000)下即每 15 秒断一次。为什么以前没暴露:我们自己的接收端(Windows 客户端)会回
pong
(T-WIN-020 实测 9 次 ping 全部收到 pong),所以这条路径在自家联调里永远走不到 ——
只有契约允许的另一种实现才会踩。修复:
onOpen复位lastPongAtMs = 0L(0 = 本连接还没收到过 pong),
只有「曾经回过pong、之后又不回了」才判定链路已死 —— 这正是该诊断本来的意图。二、新增:可注入计时,退避封顶可端到端验证
契约 §7.1 的退避序列
0.5s → 1 → 2 → 4 → 8 → 15s 封顶若要端到端验证,
真实等待约 30 秒;「连续 3 个心跳周期没收到 pong」也要等几十秒。
现在把「现在几点」与「等多久」抽成可注入的Timing:- 生产默认
SystemTiming= 真实时钟 + 真实delay(),行为与抽取前完全一致; - 测试注入「可控时钟 + 立即返回的等待」,毫秒级跑完整条序列。
三、新增 4 项 JVM 用例(无需设备)
用例 断言 退避封顶且不无限增长 第 1..8 次等待分别落在 500/1000/2000/4000/8000/15000/15000/15000的 ±20% 区间内;真实耗时 < 5 秒等待走注入实现 前三次重连真实耗时 < 1.5 秒(真实退避需 3.5 秒) 合规但从不回 pong 不被误判 走过 6 个心跳周期仍为 streaming、无「判定链路已死」日志(修复前此用例失败)回过 pong 后停止回复仍能正确诊断 出现「连续 3 个心跳周期未收到 pong」并进入重连 $ ./gradlew :app:testDebugUnitTest 测试类 7 个,用例 68 项,失败 0 # 原 64 项build/verify.sh全绿:签名、APK 版本元数据0.2.2/202、native 库体检
(ELF 类型 /DT_NEEDED闭包 / 16 KB 页对齐)、ABI 恰为两个真机 ABI、
R8 保留 MediaPipe 20169 条、dex 含PoseLandmarker。四、安装
资产 说明 opendance-android-v0.2.2.zipAPK + README.txt+THIRD_PARTY.md+contracts/快照SHA256SUMS裸文件名, sha256sum -c SHA256SUMS直接校验sha256sum -c SHA256SUMS && unzip opendance-android-v0.2.2.zip adb install -r opendance-android-v0.2.2/app-release.apk # Android 10+(minSdk 29)五、未在本环境验证(诚实声明)
- 真机 30 分钟连续推流、Wi-Fi 抖动 / 切网 / 进程被杀 / Doze 省电仍需设备实测;
Timing覆盖「时钟与等待」;OkHttp 自身的连接/写超时仍走真实时间。
Downloads
- 生产默认
-
released this
2026-09-17 05:44:07 +08:00 | 2 commits to main since this release⚠️ 本版已被 v0.2.2 取代(建议直接用 v0.2.2):
v0.2.2 修掉了「合规但不回pong的接收端被周期性误判为链路已死」——
契约 §3.6 明确pong只用于诊断,而本版会因此每 15 秒主动断链重连,
正是"频繁重连"的症状(自家 Windows 接收端会回 pong,所以联调时走不到这条路径)。OpenDance Studio -- Android 辅助 App v0.2.1
修复:重连链路的一处真实缺陷 —— 上一次会话的关闭码会污染下一次会话,
导致重新开始推流后失去自动重连能力。建议所有用户改用本版。新增:JVM 内的真实 WebSocket 重连集成测试(无需设备)。
契约快照不变:
rest-api 0.5.0/websocket-keypoints 0.2.0/data-models 0.7.0/
skeleton-topology 0.2.0/model-formats 0.5.0/database 0.4.0
(与 opendance-web / opendance-windows 逐字节一致)。一、修复的缺陷(⑦ 连接与重连)
现象:第一次连接被对端以
1001关闭(例如 Windows 端服务重启)之后,
用户再次点「开始推流」,如果这一次是网络层失败(服务端还没起来 / 被拒),
客户端不会自动重连,界面停在「已停止」,只能反复手点。根因:
ReconnectPolicy.shouldReconnect(closeCode)是按上一次记录的关闭码
判断该不该重连的(契约 §7:1000/1001/1002不重连)。而connect()只重置了
重连计数与 pong 时间,没有清lastCloseCode—— 于是本次会话的网络失败
(本应记为0= 未收到关闭帧 → 应重连)被旧值1001判成「对端主动关闭」,
scheduleReconnect()直接stopped。修复:
位置 改动 connect()每次新会话清掉关闭状态: lastCloseCode = 0、lastCloseReason = ""onFailure()记录 recordClose(0, …),使契约 §7 的「异常断开 → 应重连」生效,并让界面「最近一次断开原因」显示本次失败而非旧值为什么之前没发现:原有
ReconnectPolicyTest(11 项)验的是纯函数
「给定关闭码应该怎么退避」,而这个缺陷出在调用点(关闭码没被复位)。二、新增:真实 WebSocket 重连集成测试(8 项,JVM,无需设备)
app/src/test/java/com/opendance/studio/PoseWebSocketReconnectTest.kt:
用MockWebServer.withWebSocketUpgrade起真实 WebSocket 服务端
(能按契约 §3.2 回welcome、按 §3.6 回pong、按指定关闭码关连接),
把真实的 OkHttp WebSocket 接上去,覆盖两条分支:- 该重连的:异常关闭码
1011→ 进入reconnecting,等待时间取自策略并落在
±20% 区间,重连成功后计数清零;服务端未启动时 1.5 秒内尝试 ≤3 次(退避生效); - 不该重连的:
1000/1001/1002/ 用户主动disconnect()→stopped
且不再建立新连接(日志记录「不重连」便于真机排障); - 握手:
hello带deviceId、welcome的sessionId与保活参数解析、
按协商间隔发送应用层ping并收到pong(协议层 ping 不能替代)。
$ ./gradlew :app:testDebugUnitTest 测试类 6 个,用例 64 项,失败 0,跳过 0 # 原 56 项build/verify.sh全绿:签名、APK 版本元数据0.2.1/201、native 库体检
(ELF 类型 /DT_NEEDED闭包 / 16 KB 页对齐)、ABI 恰为两个真机 ABI、
R8 保留 MediaPipe 20169 条、dex 含PoseLandmarker。三、安装
资产 说明 opendance-android-v0.2.1.zipAPK + README.txt+THIRD_PARTY.md+contracts/快照SHA256SUMS裸文件名,下载目录内 sha256sum -c SHA256SUMS直接校验sha256sum -c SHA256SUMS && unzip opendance-android-v0.2.1.zip adb install -r opendance-android-v0.2.1/app-release.apk # Android 10+(minSdk 29)四、仍未在本环境验证(诚实声明)
- 真机 30 分钟连续推流、Wi-Fi 抖动 / 切网 / 进程被杀 / Doze 省电等真实断网场景
仍需设备实测 —— 本版新增的是「重连逻辑与真实 WebSocket 协议交互」的 JVM 级证据,
它替代不了设备; - MediaPipe 在 arm64 真机上的初始化:打包配置一侧已用 native 库体检排除
(ABI 匹配、依赖闭包完整、16 KB 页对齐),但运行时行为仍需设备。
Downloads
- 该重连的:异常关闭码
-
released this
2026-09-17 01:11:55 +08:00 | 7 commits to main since this release⚠️ 本版已被 v0.2.1 取代(建议直接用 v0.2.1):
v0.2.1 修掉了重连链路的一处真实缺陷 —— 上一次会话的关闭码会污染下一次会话,
导致重新开始推流后遇到网络失败时不再自动重连(只能反复手点)。
另新增 JVM 内以真实 WebSocket 验证重连链路的集成测试(8 项,无需设备)。OpenDance Studio -- Android 辅助 App v0.2.0
重连状态机修复(《DSH 开发文档 v1.6》6.5 / T-AND-011)、
MediaPipe 初始化崩溃修复(6.6 / T-AND-012)、
「关于」清单版本号(11 / T-AND-010)。契约
websocket-keypoints提升到 0.2.0(向后兼容:子协议名与二进制帧
version均不变,只新增可选控制字段与一种新控制类型),三端快照逐字节一致。一、连接状态异常 / 频繁重连(T-AND-011)
逐条核对后找到 3 个真实根因,都不是"退避参数没调好"这种表象问题:
根因 1:保活用错了层(最要命)
Windows 端有 5 秒静默超时,只看应用层收到的数据帧/文本帧;
而本端原先只有 OkHttp 的协议层 ping —— 它由底层库自动应答,
根本不会暴露给接收端的应用层,也就是说它不能续命。
结果:画面里没有人时 5 秒必然被踢、然后重连,循环往复。- 契约新增 1.1:接收端必须在
welcome/config中声明silentTimeoutMs与
heartbeatIntervalMs,且 心跳必须小于静默超时的一半; - 契约新增 3.6:应用层
ping/pong; - Windows 端超时 5s → 15s、期望心跳 5s,并在
welcome/config中下发; - 本端按声明值周期发
ping,连续 3 个周期没收到pong才判定链路已死并主动重连。
根因 2:不该重连的时候也重连
- 契约新增 7 关闭码策略:
1000/1001/1002与主动停止一律不重连,
避免"重连风暴"; - 退避与抖动按 7.1 冻结:0.5s → 1 → 2 → 4 → 8 → 15s 封顶 + ±20% 抖动。
根因 3:frame_id 恒为 0
PoseDetector.detect()算出safeTimestamp后忘了写回lastTimestampMs,
于是frameId永远是 0 —— 接收端据此做的丢包检测完全失效。已修。其他对齐
- 契约新增 8 连接状态机:
idle → resolving → connecting → handshaking → streaming → degraded → reconnecting → stopped,两端同名,可直接对照日志; - 契约新增 5 设备去重:
hello携带稳定deviceId(ANDROID_ID的 SHA-256
前 16 位,不含任何硬件序列号),Windows 端据此去重,"已连接 N 台"不再抖动; - 运行页显示状态机名、协商子协议、最近断开原因(close code + 中文说明)、
保活参数、心跳 RTT,并提供 「导出连接日志」(最近 500 条:状态迁移 /
close code / 子协议协商 / 首帧时间)。
二、MediaPipe 初始化崩溃(T-AND-012)
ExceptionInInitializerError: null的 message 为null,没有任何信息量 ——
真因一定在 cause 链里(通常是UnsatisfiedLinkError)。逐条核对 6.6 的五步,
本项目实际命中 三处:项 处理 拿到真因 新增 PoseDiagnostics:展开完整 cause 链取最内层真因,给出"缺少的具体库"ABI 核对 比较 Build.SUPPORTED_ABIS与 APK 内lib/<abi>/实际目录,直接指出"APK 只包含 arm64-v8a/armeabi-v7a,而模拟器(x86_64)需要 x86_64"这类必崩组合;构建期新增verifyReleaseAbi强制核对native 解压方式 packaging { jniLibs { useLegacyPackaging = true } }(AGP 8 的 release 默认extractNativeLibs=false,MediaPipe 的 JNI 加载对它有历史兼容问题)R8 细化 补 -keepclasseswithmembernames class com.google.mediapipe.** { native <methods>; },并用-printseeds输出 seeds 供发布前核对(实测保留 20169 条 MediaPipe 条目)未换库:诊断指向的是集成配置(native 解压 + R8 + ABI),不是 MediaPipe
本身不稳定,因此保留 MediaPipe(33 点契约零成本)。三、「关于」清单展示版本号(T-AND-010)
- 版本唯一来源
versionName(=project.yaml的version= APK 文件名 = Git tag);
versionCode按10000*主 + 100*次 + 修订递增(0.2.0→200); - 契约版本改为构建期解析
contracts/VERSION后注入BuildConfig
(六项契约各自独立递增,原先的单个CONTRACT_VERSION常量已失真),
关于页逐项列出;Kotlin 界面代码里不出现任何版本号字面量; - 新增
checkReleaseConsistency并挂到每个变体的preBuild:
versionName/project.yaml/contracts/VERSION/versionCode任一不自洽
直接 BUILD FAILED,不让版本漂移进入产物。
校验
- 单元测试 56 项全部通过(新增
ReconnectPolicyTest11 项、
PoseDiagnosticsTest12 项;金标准 239 字节向量测试仍逐位一致); bash build/verify.sh三步全通过:单元测试 → debug/release 构建 →
签名 / 包信息 / ABI / MediaPipe native 库 / R8 seeds / dex 关键类校验
(release ABI 恰为arm64-v8a+armeabi-v7a);opendance-android-v0.2.0.zipsha256:12db050f151a9ad6970646c5e1956918908c5662fd0dd38a8c3e12fc4d57423f
真机 30 分钟连续推流与「连续 30 分钟推理无崩溃」两项验收需要真机实测
(本环境无 Android 设备),根因修复与回归防护已由单测与构建期校验覆盖。安装
sha256sum -c SHA256SUMS unzip opendance-android-v0.2.0.zip adb install -r opendance-android-v0.2.0/app-release.apk也可直接把
app-release.apk拷到手机安装(Android 10+)。
安装后请到「关于」页确认显示v0.2.0与六项契约版本。Downloads
- 契约新增 1.1:接收端必须在
-
released this
2026-09-14 20:42:02 +08:00 | 23 commits to main since this releaseOpenDance Studio — Android 辅助 App v0.1.0
阶段三交付物:Kotlin + CameraX + MediaPipe 端侧推理。把 33 个骨骼关键点通过
WebSocket 推给 Windows 客户端。画面不离开设备。⚠ 本次修订修复了一个闪退缺陷:扫描到客户端后点击「开始推流」会立刻闪退。
附件已更新为修复版本,请重新下载安装。缺陷根因与证据
根因:
PoseDetector.initialize()只写了catch (Exception)。而 MediaPipe
初始化失败抛出的是UnsatisfiedLinkError/NoClassDefFoundError/
ExceptionInInitializerError—— 这些都属于Error而不是Exception,
会直接冒泡成进程崩溃。因此只要 MediaPipe 在设备上初始化失败,一进推流页就闪退。证据(对照实验):新增 Robolectric 组合测试后,把代码临时改回
catch (Exception):改回 catch (Exception):5 项测试 2 项失败 java.lang.UnsatisfiedLinkError: no mediapipe_tasks_jni in java.library.path java.lang.NoClassDefFoundError: Could not initialize class ...PoseLandmarker 改为 catch (Throwable):5 项全部通过本次修复与加固
PoseDetector/CameraController全部改为捕获Throwable,失败只提示不崩溃- 移除
accompanist-permissions:0.34.0(2024-02 发布,会传递引入
composeanimation-core:1.6.0,与 Compose BOM 存在版本偏斜风险),
改用平台 Activity Result API - MediaPipe 回调运行在相机线程:所有 Compose 状态写入统一 post 回主线程
- WebSocket 连接、相机启动、单帧分析全部加失败保护,单帧异常不再中断推流
- 新增 CrashLogger:未捕获异常落盘到
filesDir/crashes/,
并在连接页直接展示堆栈 —— 免去连 adb 抓 logcat - 新增 Robolectric 组合测试:真实组合连接页/运行页/关于页,
专门覆盖「进入页面即崩溃」这一类编译期看不出来的缺陷
验收结果
单元测试 ✓ 30 / 30 通过(含 5 项 Robolectric 界面组合测试) 跨端金标准向量 ✓ Kotlin 序列化器与 Web 端 TypeScript 实现逐位一致 ./gradlew assembleRelease ✓ 生成已签名 APK(28.6 MB) apksigner 校验 ✓ 退出码 0,v2 签名方案 包信息 ✓ com.opendance.studio v0.1.0,minSdk 29 / targetSdk 34 模型随包分发 ✓ assets/pose_landmarker_lite.task 5.7 MB(Stored 未压缩) ABI ✓ arm64-v8a + armeabi-v7a已排除的可能:Lint 无
NewApi/MissingClass问题;native 库齐全且全部
Stored + 4096 对齐(dlopen不会因对齐失败);LocalLifecycleOwner等符号
在 dex 中存在;animation-core解析为 1.7.5 而非有缺陷的 1.6.0。使用
- 手机安装 APK(Android 10+)
- 电脑启动 Windows 客户端(自动监听 8765)
- 手机与电脑连同一 WiFi,应用内「扫描局域网」自动发现,或手填 IP
- 点「开始推流」并授予相机权限
若仍闪退:重新打开应用后,连接页顶部会直接显示上次崩溃的完整堆栈
(或用adb logcat -b crash抓取),把它反馈即可继续定位。已知事项
- 未在真实 Android 设备上安装运行(构建环境无设备、无模拟器、无 KVM)。
已验证到:构建通过、签名有效、包信息正确、模型与 native 库正确打包,
以及三个页面在 Robolectric 下真实组合不崩溃。 - 主机发现依赖同一 WiFi;路由器开启 AP 隔离时无法互访。
Downloads