
十轮排查定位一个无声用WorkBuddy 接入第五种视频播放平台作者岗位Java 后端 / 全栈开发视频监控平台方向项目性质企业级视频监控播放服务多平台视频接入 云台/对讲/告警代理摘要本文记录我用 WorkBuddy 完成的一次完整平台接入从零新增第5 种视频平台EasyNVR到完成实时预览、历史回放、云台、对讲、告警、保活回收全链路并在后续联调中通过十轮排查定位一个对讲成功但设备无声的疑难问题。最终产出后端新增 20 个类、43 个测试用例全部通过其中 41 个单元测试 2 个真实连平台的联调测试前端新增 7 个组件/工具模块生产构建通过。本文所述项目为内部系统文中平台地址、账号、通道号、设备指纹等均已脱敏或以「A平台/ 通道 001」代称。一、输入材料接入前我手上有什么这个播放器服务已经支持 4 种接入方式大华 NVR 直连、海康 NVR 直连、大华 ICC 平台、萤石云架构是「一套 API 屏蔽平台差异」后端按accessType分发统一返回playerType url other前端按返回的类型自动选择播放内核。要接入第 5 种平台 EasyNVR我需要平台接口文档——官方是Apifox 分享链接、带密码WebFetch直接取返回未开放内容现有代码的约定——PlayerReq已有的字段、ICC/萤石回放怎么分发、RsaAuth拦截器覆盖范围设备侧约束——现场是海康球机通过 GB28181/UDP 接入关键前置判断这一步由 WorkBuddy 读代码后给出直接决定了后面少走多少弯路PlayerReq里已有sessionId / playType / startTime / endTimeEasyNVR 的回放可以完全复用零新增字段ICC 和萤石的回放实现就是playback.equals(req.getPlayType())分发EasyNVR 对齐同一模式即可平台连接信息IP/端口/账号/secret全部走 token 载荷不进配置文件、不下发浏览器天然支持多平台实例如果这三个判断错了新增平台就要动PlayerReq、改签名、动鉴权链路——后面所有联调都要重跑。二、WorkBuddy 配置与工作方式我用的是项目级记忆 定时任务 playwright 实测的组合能力用途工作区记忆.workbuddy/memory/*.md每天的方案决策、踩坑记录、遗留项保证跨天开发不丢上下文Skill技能封装把「抓取 Apifox 文档」「调ZLM 推流」「跑 playwright 实测」固化成可复用流程playwright-core浏览器自动化复现浏览器侧疑难问题闪屏、静音、卡死这是纯靠日志推不出来的部分定时任务编译验证、单测、构建的节奏化执行补充说明Apifox 带密码的文档我是通过分享链接的llms.txt拿到全部接口的.md索引再逐个抓取 OpenAPI 3.0 原文的——这个技巧被固化进了一个技能后续接任何 API 文档都能复用。三、操作步骤含中途调整与问题处理3.1 方案先行确认 8 个问题再动手在写代码前我让 WorkBuddy 先深读现有代码把 EasyNVR 接入的 8 个待确认问题逐条给出结论并写入docs/EasyNVR播放器接入方案.md。其中三条最有价值①ssrc直接用sessionIdGB28181 回放要求ssrc作为会话标识贯穿「查询→ 倍速 → 停止」。方案结论是直接把PlayerReq.sessionId透传全链路复用不单独生成。但同时预留了降级路径如果联调发现平台校验 GB28181 的 10 位数字格式从 sessionId 哈希映射一个 10 位数字。这个降级预留后来真的用上了——见3.3。②disk_id写死但可配官方文档示例值是 6没有语义说明。我没有猜它的含义而是写死成后端常量 留easynvr.disk-idyml 配置项联调时可调。③ 回放 seek 重新拉流官方文档没明说 flv 流能否 seek。我的判断是不赌平台能力seek 直接重新调/playplayTypeplayback、startTime目标时间、复用同一 sessionId暂停/恢复/倍速走平台 GB28181 控制接口。这样无论平台支持与否功能都成立。3.2 保活机制解决用户直接关浏览器的泄漏方案定稿后我意识到一个真实问题用户几乎不会点关闭按钮都是直接关浏览器/切页面前端destroy()里的停止回放调用覆盖不到这条路。而 GB28181 回放会话持续占用设备/NVR 的回放资源单通道并发通常只有 1 路——泄漏一个会话同一通道的后续回放会直接失败提示资源占用/会话达上限直到平台超时释放或设备重启。最终方案是应用层 HTTP 心跳前端每 15sPOST /player-api/easynvr/heartbeat后端EasyNvrSessionRegistry内存表记录lastActiveScheduled每 15s 清扫45s3 个心跳周期无心跳 → 自动调平台停止预览/停止回放pagehide时用navigator.sendBeacon发一条尽力而为的离场通知一处安全加固心跳接口不走 RSA避免每 15s 加解密开销那裸sessionId就可以被伪造续命。分析下来伪造虽不能创建会话play 有 RSA、不能停止他人会话停止接口有 RSA、拿不到数据但能给已知会话无限续命正好造成我们要防的资源泄漏。所以play()时生成一个随机heartbeatKeyUUID随PlayRes.other下发心跳必须携带不匹配直接拒绝。宕机恢复分两层主会话表落盘easynvr-sessions.json登记/注销/清扫时写入用临时文件 原子移动。服务启动时加载对已过期会话逐个调平台停止——用的是宕机前缓存的完整凭证不受 token 30 分钟有效期限制辅后端重启后前端还活着的话心跳会带原 token注册表未命中时解密重建会话残余风险我如实记录了宕机 前端同时关闭 落盘文件也损坏的极端情况残留会话只能等平台超时或后台手动清理。方案里没写彻底解决。3.3 联调ssrc 校验与CLOUD 回放的两个坑ssrc 位数联调报「SSRC需要大于 20 位且不能重复」且平台接受的是纯数字。改法是sessionId经 SHA-256 →BigInteger→mod 10^24→%024d补齐。关键是必须是确定性的——同一 sessionId 每次算出同一个值否则倍速和停止接口引用同一个会话就找不到人了。降级预留刚好用上了。CLOUD 源回放的 400这个坑我拆成了三层排查CLOUD 源不接受disk_id那是本地磁盘录像参数传了就 400去掉disk_id后 200 了但返回的url叫.m3u8实际响应是JSON 分段列表hls.js 播不了真正的播放列表在hls_url改用hls_url后报manifestParsingError——因为平台返回的 url 带查询串...index.m3u8?...sourceCLOUD我的.endsWith(.m3u8)判断被查询串干扰没触发。截掉?再判断路径解决仍失败——那时段平台云存没有录像hls_url返回的是空播放列表。前端映射为「该时间段无录像数据」最后我加了一条回归测试同时断言「CLOUD 不带 disk_id」和「JSON url 换 hls_url」防止回退。3.4 浏览器侧疑难问题为什么必须用 playwright有两类问题纯靠日志推不出来必须实际在浏览器里复现首开闪屏 喇叭失效。用 playwright-core Edge channel 实测复现首开报addSourceBuffer 达到上限F5 后正常定位到两个根因FLV 元数据分次下发音频轨晚于视频出现mpegts.js 中途补建 audio SourceBuffer 报 limit。此前我的降级逻辑把这类瞬态错误切到 jessibucacanvas 接管画面重载感。改成降级只收窄到编解码层错误SourceBuffer/MSE 类错误走 800ms 后自动重连重建video上限 2 次才降级jessibuca 3.x 的unmute()是无参调用带 bool 参数实际不生效静音切换失效直播卡死看门狗。对讲时画面偶尔永久停在最后一帧——GB28181 侧音频广播触发重新 INVITE部分设备会让平台重启推流ws-flv 连接挂着但不再来数据且无 ERROR 事件。加了看门狗playing后每 2s 采样currentTime连续 3 次无推进判定卡死走同一套重连。https 对讲不出声mixed content。https 页面点对讲立刻变不安全。根因是对讲和 ws-flv 的地址都是平台返回的相对路径后端用http/ws平台 host 补全Chrome 判定混合内容拦截。同时揪出一个老 bugtalk()用了http://前缀补全talk_url而new WebSocket(http://...)直接 SyntaxError——这个对讲从来没真正连上过。最终走 nginx wss 反代 stream-host传参重写解决。3.5 十轮排查一次对讲成功但设备无声这是整个项目最难的一环。前端talkSuccess正常触发、设备无声、没有任何报错。我按下面的顺序推进第 1-2 轮地址选错。前端选对讲地址的逻辑是JSON.parse(token)取 streamHost——但 token 是 RSAAES 加密串JSON.parse必然失败返回空导致永远落到公网地址而主码流走的是内网地址会话错位。改为按wsURL的 hostname 匹配与主码流同源兜底匹配响应innerIp。第 3-5 轮缺参数。读官方 SDK 源码WSPlayer.js / PlaySDKInterface.js发现两处官方采集链路getUserMedia失败被.catch(function(e){})静默吞掉表现就是对讲中但无声且无报错官方通道模式会把StartTalk响应的sampleRate/audioBit传给内部编码器而 talkByUrl 模式不传编码器拿到 undefined。补上麦克风预检 默认 8000/16。第 6-8 轮请求体对齐。用户提供了 ICC 平台官方 StartTalk 载荷逐字段对比发现我们缺channelSeq、talkmode/target/source/broadcastChannels、enableGBParamAutoAdapt且audioType应是数字2不是字符串2。补上后仍无声于是开 SDK verbose 日志localStorage.playSDKLogLevel6做客户端逐行对比我们这边 OPTIONS/DESCRIBE 阶段反复报invalid onvif talk media index:-1, or back stream src is nullICC 网页这边一切正常且DataSourceManager find live data src: talk/pu/237命中结论是服务端会话状态不同 → 请求体仍不一致。继续对齐官方 WINPC 抓包载荷发现talkType官方是字符串1我们传数字导致会话异常、缺optional携带平台登录 token 做 MTS 会话绑定、多传了gbDevice、需要 PSDK 顶层信封。第 8 轮的关键证伪我保持对讲时在 ICC 页面对同一设备发起对讲报「设备正在对讲中」——说明 MTS 真正注册了我们的会话客户端链路已完全等价。嫌疑收敛到三项后端代理账号权限、音频内容全静音、MTS→设备转发段。用户复查system是超管全权限排除第一项。第 9-10 轮结案。我在麦克风预检里加了 RMS 电平实测getUserMedia → AnalyserNode 采样 800ms 算 peak RMS。第一版测出-inf但发现是假静音AudioContext在非用户手势的异步链路里创建会suspendedAnalyserNode 读数全 0。升级预检后resume() 等待running 默认静音约束时改用原始约束重测 打印轨道 label才拿到真实结论根因是系统选错了麦克风输入设备浏览器采集到的是静音轨道。设备收到全 0 的 G711 流 → 无声、无报错、talkSuccess照常触发。用户切换正确麦克风后对讲正常。这里有个容易误判的点值得单独记talkSuccess在 RTSP PLAY 完成前就会触发不能作为「会话就绪」的判据——早期音频帧会被丢弃日志里 seq 从 11 起说明前 10 帧已被丢。沉淀下来的排查顺序是地址 → 参数对照官方抓包 → 会话注册验证设备忙测试→ SDP/RTP 层逐行对比 verbose 日志 → 麦克风 RMS 实测。四、产出物类别内容后端新增 20 个类PlayerParamEasyNVR、EasyNvrApiClientBasic 鉴权 流地址补全 ssrc 映射、EasyNvrSessionRegistry心跳/清扫/落盘恢复、EasyNvrService/Impl、EasyNvrController7 个接口等改PlayerServiceImpl分发分支前端新增 7 个模块EasyNvrBox、EasyNvrPlaybackBox自绘回放控制条、utils/g711a.jsA-law 编码 8k 降采样约 50 行、api/easynvr.js含心跳器等MpegtsPlayer增加 live 模式区分、降级策略、重连、卡死看门狗方案文档docs/EasyNVR播放器接入方案.md含 8 个确认问题结论、平台接口核实结果、遗留联调项收缩到 4 项、以及如实记录的残余风险测试6 个测试类43 个用例全部通过含 ssrc 确定性断言、CLOUD 回放回归、stream-host 重写用例验证vite build生产构建通过playwright 全流程实测通过对讲静音图标联动、30s 无看门狗误报、停止后恢复顺带沉淀的可复用方法浏览器疑难问题的 playwright 实测流程chromium 缺失时用channel: msedge、SDK verbose 日志开关定位法、RMS 麦克风实测含 suspended 假静音陷阱。五、几点体会方案先行比写代码值钱。8 个确认问题里3 个直接影响了架构决策复用 PlayerReq、ssrc 透传、seek 策略。如果边写边定这些改动会连锁影响鉴权、签名和测试。把平台文档说的和平台实际做的分开。文档说停止回放必须显式调但没给路径说 flv 可能不支持 seek所以干脆不赌。文档示例的disk_id6没解释语义那就写死可配。这些不确定性在联调里都会变成调试成本。用户不会点关闭按钮。任何依赖前端钩子做资源回收的设计在真实使用中都会漏。保活机制不是锦上添花是 GB28181 单通道并发1 这个约束下的必需品。「成功回调」不等于「功能可用」。talkSuccess早于 PLAY 完成addSourceBuffer的瞬态错误被误判为不可恢复错误导致画面重载unmute(bool)在 3.x 里静默失效。三个 bug 共同指向一件事不要相信没报错和回调触发了要实测。排查疑难问题时证伪比枚举更快。「保持对讲时 ICC 页面报设备忙」这一句话直接洗清了整个浏览器侧链路把嫌疑从五项收敛到三项。如果没有这个实验后面可能还在前端继续挖。