AI 写了一个BUG,球机弹框「关闭重开黑屏」排查复盘

视频弹窗「关闭重开黑屏」排查复盘

背景:某视频监控类 Web 项目中的球机播放弹窗(演示模式:下拉选择摄像机播放),功能上线后发现「刷新后首次打开能播、关闭面板再打开黑屏、下拉切换能播」,前后排查数十轮,最终定位为前端播放触发逻辑缺陷。本文为三个问题的诚实复盘。

问题 1:当初写代码时犯了什么错

核心错误:把"播放触发"写成了依赖时序巧合,而不是显式事件。

错误 具体表现 后果
依赖隐式时序 播放触发 = onMounted → setupPlayer,但 setupPlayer 能否工作依赖 selectedCamera 恰好已被 watch demoCameras(immediate) 设置好 弹窗 v-if 销毁重建(关闭再打开)时,这条"恰好"链条断裂 → 播放被跳过
静默失败 setupPlayerif (!cameraNo) return ------无日志、无兜底 失败完全无声,用户看到黑屏,代码无任何痕迹,为后面十几轮排查埋雷
忽略核心生命周期 弹窗的显隐是 visible prop(打开/关闭),这是它真正的生命周期事件,却用 onMounted(只执行一次)+ watch 链代替 visible 从 false→true 没有任何代码响应,重开=重新挂载=重走脆弱链

一句话 :该用 watch(visible) 驱动播放(显式、每次打开都触发),却写成了 onMounted + 依赖 selectedCamera 恰好就绪(隐式、时序脆弱)。

问题 2:为什么查了这么多轮,从什么时候开始找到原因

分两个阶段:

阶段一:完全在错误层面打转,约 8 轮。

用户反馈"关闭重开失败"后,尝试了:黑屏检测、弹窗 v-show 常驻、防重入、对照组件同构、1s 延时------全部在"播放机制"层面改(重试/重建/时序),没有一次去验证"重开时播放代码到底执行了没有"。期间还带着"流媒体服务僵尸流"假设查了一轮后端。

阶段二:开始取证,2 轮找到。

  • 第一步:在 setupWebrtc 入口加日志
  • 第二步:用户抓的第一份日志出现关键迹象(重开时刻无 ICE 日志)
  • 第三步:走查代码发现 setupPlayer 的静默 return 点
  • 第四步:实施修复(watch visible 显式触发 + 代际机制)
  • 第五步:完整日志确认"重开时 setupWebrtc 从未执行"
  • 第六步:实测通过

结论:真正定位从第一份带入口日志的 Console 输出开始,之后走查代码确认,最后日志铁证。

问题 3:为什么之前多轮没找到

原因 说明
没先取证,靠猜 一直"猜根因→改→让用户测",没有先设计"一次测试拿全证据"的手段。若第一次收到反馈时就加全入口日志,一轮就能看到"setupPlayer 没执行或 cameraNo 空"
混淆两类问题 "播放失败"(连接建了但没画面)vs "播放没被触发"(代码根本没跑)------一直在按前者修。最直接的证据被浪费 :黑屏检测挂在 ontrack 里启动,播放没开始它永远不会触发------"黑屏检测从不触发"本身就是线索,没抓住
"切换能播"没追到底 这是最强线索:切换走 watch cameraNo 路径能播 = 播放逻辑本身没问题,问题在触发源。应由此直接对比"打开路径"和"切换路径"的差异,而不是去查流媒体服务/ICE
覆盖式修复掩盖真相 videoRef 重试"修好"了首次打开,但让"触发链脆弱"这个本质问题藏得更深------后续机制越叠越多(重试+黑屏检测+play+v-show+常驻),越难看出"重开时压根没触发"
内网环境放大成本 看不到实时状态,每轮只能靠用户反馈,"猜-改-测"循环成本极高。正确做法是一次性加全关键路径日志,一轮测试拿到完整链路证据

沉淀的教训

  1. 编码 :组件显隐是生命周期事件,用 watch(visible) 显式驱动,不依赖 onMounted + watch 链的时序巧合;失败路径不允许静默 return。
  2. 排查:先取证后改码;机制无效本身就是证据(黑屏检测不触发 = 播放没开始);"X 能播"要追到"X 走了哪条代码路径",用它对比失败路径,而不是拿去猜服务端。

最大教训:"能跑"和"对"是两回事------代码在"首次打开"这个时序下能跑,不代表它是对的,它依赖的时序假设在其他场景(关闭重开)会断。