Open Headunit ProjectionWatchdogPolicy投屏看门狗:画面假死如何被检测与重启
【免费下载链接】open-headunitHeadunit App for displaying Android Auto项目地址: https://gitcode.com/GitHub_Trending/he/open-headunit
Open Headunit(Headunit Revived)是一个把 Android 平板变成 Android Auto 中控的开源投屏应用。它的核心组件投屏看门狗 ProjectionWatchdogPolicy,专门负责检测投屏画面"假死"(黑屏、卡住不动)并自动向手机重新索要视频流,让你开车时不再手动折腾。
什么是投屏"假死"?先认识这种故障
用 Android Auto 投屏时,你可能遇到过这类情况:
- 📺 画面突然卡住不动,或变黑屏,但音乐还在放
- 📵 导航界面定格在某一张图,怎么点都没反应
- 🔄 需要"杀 App 重开"才能恢复,甚至直接重启车机
这就是"假死":手机和车机之间的连接可能还活着,但视频流停了,画面再也不刷新。对开车的人来说,一个不再更新的黑屏比没连接更危险。
Open Headunit 的答案是一个常驻的"看门狗":每隔 2 秒巡逻一次,发现画面异常就自动执行恢复动作。核心决策逻辑集中在 ProjectionWatchdogPolicy.kt 这个文件里。
看门狗如何检测:每秒都在"数心跳"
投屏看门狗并不是简单的"超时重连",它把"画面停没停"和"连接断没断"当成两个独立问题来判断。这是整个设计最聪明的地方。
心跳一:最后渲染帧距今多久
看门狗会记录解码器最后一次把画面渲染到屏幕上的时间戳。如果距今超过10 秒(常量FRAME_GAP_MS),就判定"画面已停"。
心跳二:整条链路静默了多久
这里有个 Android Auto 的"坑":当屏幕上没有任何内容在动时(比如暂停的播放器),手机会彻底停止发送视频,但连接依然健康。如果只看"多久没收到帧",看门狗会把正常待机误报成断线——这正是历史上 issue #852 的由来:音乐暂停 15~30 秒,画面就被盖上"连接丢失"的遮罩。
所以第二个心跳测量的是"手机在任何通道上最后一次发消息距今多久"(LINK_QUIET_MS,10 秒)。只有两个条件同时满足才会向用户显示"正在重连":
| 现象 | 帧间隔 > 10s | 链路静默 > 10s | 看门狗的结论 |
|---|---|---|---|
| 音乐暂停待机 | ✅ | ❌ | 正常,静默恢复 |
| 视频流中途死亡 | ✅ | ✅ | 判定断连,提示用户 |
| 画面正常播放 | ❌ | — | 什么都不做 |
判断函数 shouldShowReconnecting 就一行:两个阈值必须都超过才报警。
自动重启恢复:黑屏不用等,主动"要画面"
检测出来只是第一步,真正的价值在于恢复。
重新索要视频焦点
当"画面已停"被确认时,看门狗会主动向手机发送一条VideoFocusEvent(gain = true, unsolicited = true)消息——相当于对手机说:"请把视频再发给我。"这条请求受到节流保护(见 VideoRecoveryPolicy),避免每个 2 秒的 tick 都轰炸手机。
注意一个细节:恢复动作只看画面停没停,不看遮罩显没显示。因为"看起来像待机"的卡顿流和真正停掉的流长得很像,如果恢复动作绑在提示遮罩上,恰好会把该救的流漏掉。
显示器卡死:重建投屏视图
另一类假死出在车机端:手机还在发视频,但某些车机(尤其是 MediaTek 芯片的 GLES 模式)的显示消费端"罢工"了,画面冻结(常见于定格在 Android Auto 启动 Logo 上),音频却正常——这是 issue #650。
看门狗为此设了双重探测:
- 完全冻结:连续5 秒(
DISPLAY_FREEZE_MS)没有任何画面被绘制 - 吞吐崩溃:滑动窗口(10 秒)内出现 ≥10 个异常长帧——MediaTek 上表现为掉到 2~5fps,单纯"5 秒无帧"检查抓不到
触发后,看门狗会自动重建投屏视图(等价于手动"Home + 重新打开")。如果重建后仍反复卡顿,会升级到 SurfaceView 后端(本会话内生效),最多尝试 4 次,避免无限重建。这段逻辑在 AapProjectionActivity.kt 的maybeRecoverFromDisplayStall中实现。
第一帧催促:黑屏起步也能被救
新会话刚开始、画面还没出来时,还有一条"首帧催促循环":只要会话存活、surface 已就绪、但画面上还没有可信的画面(灰屏不算),看门狗就每隔 1.5 秒催一次手机发关键帧,直到画面出现或升级策略接手(见 shouldNudgeForFirstFrame)。
看门狗自己也"死"过:一段真实修复史
值得讲一个小故事,因为它解释了为什么看门狗被单独抽成一个 Policy 类。
早期的内联检查只认HandshakeComplete这一种连接状态。但握手完成只是视频开始前的短暂窗口,整个行驶过程的稳定状态是TransportStarted。结果看门狗在每个会话的第一个 tick 就"判自己死亡"、不再自我调度——挂在它身上的所有恢复机制(重连遮罩、显示卡顿恢复、渲染器确认提示)在正常会话里全都不可达。视频流中途挂掉后,屏幕就一直黑着,再也没人开口要。
复活看门狗后它又立刻犯了另一个毛病:只凭"帧间隔"就把 Android Auto 的正常待机叫成断线(issue #852)。最终方案是把两个决策拆开:画面停值一次恢复请求,但不一定值得弹遮罩。这些边界条件都被逐条钉死在单元测试里:
- 状态表测试:ProjectionWatchdogPolicyTest.kt
- 链路静默监测(诊断用):LinkGapMonitor.kt
关键参数一览表
| 常量 | 数值 | 作用 |
|---|---|---|
WATCHDOG_TICK_MS | 2 秒 | 看门狗巡检周期 |
FRAME_GAP_MS | 10 秒 | 画面停摆判定阈值 |
LINK_QUIET_MS | 10 秒 | "链路静默"判定阈值 |
DISPLAY_FREEZE_MS | 5 秒 | 显示端冻结判定阈值 |
LONG_FRAME_WINDOW_MS | 10 秒 | 长帧滑动统计窗口 |
所有数值均可在 ProjectionWatchdogPolicy.kt 中查到,每个常量旁都有注释说明它从哪次真实故障中得来。
投屏卡死时你应该做什么
大多数情况下什么都不用做——看门狗会在 10 秒内自行恢复,你可能只会看到短暂黑屏或一次"正在重连"提示。如果问题持续:
- ⚙️ 在设置中切换视图模式(TextureView / SurfaceView / GLES),看门狗升级失败时的兜底就是切到 SurfaceView
- 📄 通过应用的日志导出功能导出日志(LogExporter.kt),日志里的看门狗行会写明"画面空闲了 Xms,但链路 Yms 前还在说话",方便定位
- 🔁 极端情况下退出重进投屏界面,看门狗会在每次进入时重新武装
小结
Open Headunit 的投屏看门狗把一个容易"看一次算一次"的玄学问题,变成了一组可测试、可追溯的规则:
- 检测:帧间隔 + 链路静默双心跳,区分"真断线"和"正常待机"
- 恢复:主动索要视频焦点、重建投屏视图、升级渲染后端,层层递进
- 克制:节流、次数上限、冷却期,防止看门狗自己变成黑屏的制造者
下次投屏黑屏时,不妨多等 10 秒——它很可能已经在路上了。🚗
【免费下载链接】open-headunitHeadunit App for displaying Android Auto项目地址: https://gitcode.com/GitHub_Trending/he/open-headunit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考