故障排查
故障排查
国标接入的问题几乎都落在同一条链路的某一段上。本章按 「设备接不进来 → 通道拉不到 → 视频放不出来」 三段组织,每个故障给出可能原因、排查命令/步骤、解决办法,最后附一份通用排查工具箱。
定位的关键是先把故障归到链路的某一段、再对症排查:先用会话状态与抓包确定卡在"哪一段",再针对该段深入——多数排查耗时,都消耗在对故障所处环节的误判上。
一、先认识这条链路
设备注册(REGISTER/401/200)
→ 目录查询(MESSAGE Catalog,落 iot_vms_channel)
→ 点播(INVITE + SDP,握手 100/200/ACK)
→ 设备推流(RTP/PS → 媒体节点收流口)
→ 媒体节点转协议(ws-flv / hls / rtmp)→ 浏览器出图| 哪一步断 | 现象 | 看哪里 |
|---|---|---|
| 注册 | 设备配了上级却连不上、平台无注册日志 | lsof :5060、SIP 抓包、iot_vms_device |
| 目录 | 注册上了但通道为空 | SIP 抓包(Catalog)、iot_vms_channel、charset |
| 点播信令 | 会话卡 WAITING 且 sip_call_id 为空 | SIP 抓包(INVITE/200) |
| 收流 | 会话 WAITING、sip_call_id 非空,媒体节点无流 | ZLM getMediaList、收流口连接、Docker 端口映射 |
| 转协议/回调 | ZLM 已收流但会话仍 WAITING | ZLM Hook(on_stream_changed)、流名归一、stream ready 日志 |
下文命令里的占位符:
<节点IP><HTTP端口>(媒体节点 REST 地址,Docker 部署填对外映射端口)、<secret>(ZLM[api] secret)、<nic>(平台 SIP 监听网卡,通常en0)、<设备IP>、<deviceId>、<zlm容器名>。
二、摄像头对接不上(注册失败)
现象:设备端配置了上级,但平台收不到注册、设备一直显示离线;或反复 401、被回 403。
可能原因(按命中率排序)
- 对外信令地址(
host)探测错 / 配错。vms.gb28181.sip.host留空时自动探测本机 IP;多网卡 / Docker Desktop 环境容易选中虚拟网卡(如192.168.65.1、Docker bridge),写进出站 Via/Contact 后设备回包发不回来。 - SIP 端口没监听 / 监听网卡(
nic)配错。vms.gb28181.sip.nic是监听网卡(0.0.0.0=所有),port默认 5060。 - 传输模式不匹配。设备用 TCP 而平台
vms.gb28181.sip.transport=UDP(或反之);BOTH双栈最稳。 - 鉴权失败:
iot_vms_platform.password与设备端不一致(摘要不匹配回 403);或注册模式register-mode=MANUAL而设备未在白名单(回 403)。 - 设备端配置错:上级 SIP 服务器编号 ≠ 平台
server-id、上级 SIP 域 ≠domain(独立配置项,通常约定取 server-id 前 10 位)、上级地址/端口、注册密码填错。
排查命令
# 1) 平台 SIP 端口是否真的在监听(源码态在宿主机跑)
lsof -nP -iTCP:5060 -sTCP:LISTEN
lsof -nP -iUDP:5060
# 2) 平台对外公布的 SIP 监听地址(前端 SIP 平台页 / 接口读取),核对是不是真实 LAN IP
# 若是 192.168.65.1 这类虚拟网卡 → 显式配 vms.gb28181.sip.host
# 3) 抓 SIP 包,看设备 REGISTER 有没有到、平台回了 401 还是 403、设备有没有跟进
sudo tcpdump -i <nic> -A -s 0 "host <设备IP> and port 5060"
# 4) 平台是否落库 / 当前在线状态(务必带 is_deleted=0)
docker exec mysql mysql -uroot -proot bladex_iot -e \
"SELECT device_id,transport,ip,port,online_status,register_time FROM iot_vms_device WHERE device_id='<deviceId>' AND is_deleted=0\G"blade-server 日志关键字(均为中文):国标设备注册成功 / 摘要校验失败,已拒绝接入(403) / 未在手动注册白名单内,已拒绝接入 / 注册 nonce 过期。
解决办法
host选错 → 在application-xxx.yml显式配vms.gb28181.sip.host: <真实 LAN IP>,重启。- 端口未监听 → 检查
vms.enabled=true、nic、port、防火墙。 - 403 → 核对平台与设备的注册密码、SIP 编号/域;
MANUAL模式补白名单或改AUTO。
三、通道获取不到(目录 Catalog 为空)
现象:设备注册成功、在线,但通道列表 / 设备详情「通道」Tab 是空的。
可能原因
- UDP 目录为空、TCP 正常。大目录 UDP 易分片丢包,且部分设备 UDP 下字符集声明缺失。优先把设备/平台信令切到 TCP。
- 字符集不对。海康 GB/T 28181-2022 设备多用 GB18030 / GB2312(非 UTF-8);平台
vms.gb28181.sip.charset配GB2312。解码已下沉到应用层(SipBodyCharset/Gb28181CharsetDetector),不再做整包转码。 - 设备没应答 Catalog。平台发了查询但设备没回(日志只见
Catalog query sent、无后续)。 - 级联场景的通道授权 / 区划过滤导致子树不下发。
排查命令
# 1) 抓包看 Catalog 一问一答(平台 MESSAGE CmdType=Catalog → 设备 MESSAGE CmdType=Catalog 应答)
sudo tcpdump -i <nic> -A -s 0 "host <设备IP> and port 5060" | grep -aE "Catalog|<DeviceID>|<Name>"
# 2) 通道是否落库
docker exec mysql mysql -uroot -proot bladex_iot -e \
"SELECT channel_id,name,online_status FROM iot_vms_channel WHERE device_id='<deviceId>' AND is_deleted=0\G"解决办法
- 设备/平台改 TCP(
transport=TCP,设备端同步)。 charset配GB2312;确认 SIP body 解码正常(中文通道名不乱码)。- 设备始终不回 Catalog → 检查设备端目录订阅/能力,或主动重发查询。
四、拉流失败 / 点播一直「等待中」(WAITING)
这一段链路最长、最容易出问题。先定位卡在哪一子段,再查。
4.1 一步定位:看会话状态 + 握手是否建立
docker exec mysql mysql -uroot -proot bladex_iot -e \
"SELECT ssrc,stream,session_status,sip_call_id IS NOT NULL AS dialog_ok \
FROM iot_vms_stream_session WHERE session_status IN ('WAITING','RUNNING') ORDER BY create_time DESC LIMIT 3\G"| 结果 | 卡在哪 | 跳到 |
|---|---|---|
WAITING 且 dialog_ok=0 | 点播 INVITE 握手没成(信令段) | 4.2 |
WAITING 且 dialog_ok=1,但 getMediaList 无流 | 设备没把流推到收流口(收流段) | 4.3 |
getMediaList 有流但会话仍 WAITING | ZLM 收到了但会话没转 RUNNING(回调/流名段) | 4.4 |
报 address already in use | 收流口撞口 | 4.5 |
4.2 INVITE 握手不成(dialog_ok=0,收不到 200 OK)
最常见是 TCP 设备源端口时效:设备 TCP 连接断了重连、换了临时源端口,却没重新注册,平台记录里的端口就陈旧了;点播 INVITE 按陈旧端口去拨,落到已关闭的死连接,设备收不到、自然不回 200 OK。
# 对比:库里记的设备端口 vs 当前真正活动的 TCP 连接端口(务必带 is_deleted=0)
docker exec mysql mysql -uroot -proot bladex_iot -e \
"SELECT port FROM iot_vms_device WHERE device_id='<deviceId>' AND is_deleted=0"
lsof -nP -iTCP:5060 | grep "<设备IP>"
# 两者不一致 → 即属此故障
# 抓包看 INVITE 出没出、设备回没回(100/200/4xx)
sudo tcpdump -i <nic> -A -s 0 "host <设备IP> and tcp port 5060" | grep -aE "INVITE|SIP/2.0"平台已内置修复:每次心跳都按来源(Via rport)刷新设备
ip/port/transport,使出站请求始终路由到当前活动连接。若仍偶发卡住,等一次心跳周期(默认 60s)让端口刷新后再点播。
4.3 设备没推流到收流口(dialog_ok=1,但媒体节点无流)
握手成了、设备答应推流,但 ZLM 收不到——几乎都是收流模式与 Docker 端口映射不匹配。
- 多端口模式(
媒体节点.rtp_port=0):每路流由 ZLM 动态分配独立端口,Docker 若只映射固定端口、没放行整个端口段,设备就连不到这个动态端口。 - 单端口模式(
rtp_port>0,推荐 Docker 部署):所有取流共用这一个固定收流口、ZLM 按 SSRC 分流,Docker 只需放行该口,且须与 ZLMconfig.ini [rtp_proxy] port一致。
# 1) ZLM 是否建起了这路流(空 data = 没收到)
curl -s "http://localhost:<HTTP端口>/index/api/getMediaList" --data "secret=<secret>"
# 2) 容器内收流口(以 10000=hex 2710 为例)有没有设备连入(ESTABLISHED)
docker exec <zlm容器名> sh -c "cat /proc/net/tcp /proc/net/tcp6" | awk '$2 ~ /:2710$/{print $2,$3,"state="$4}'
# 只有 state=0A(LISTEN)= 没人连进来;有 01(ESTABLISHED)= 设备已连入
# 3) 收流口从外部可达性
nc -z -w 2 <节点IP> 10000解决:Docker 部署用单端口——媒体节点「RTP 收流端口」填 10000,docker run 加 -p 10000:10000 -p 10000:10000/udp,ZLM config.ini 的 [rtp_proxy] port=10000。
4.4 ZLM 已收流,但会话不转 RUNNING
媒体在流(getMediaList 有流、bytesSpeed 在涨)但会话仍 WAITING、前端拿不到播放地址,通常是两类:
- 回调没到平台 / secret 不对。平台共需配
on_publish/on_stream_changed/on_stream_none_reader/on_server_keepalive四个 Hook(详见 config.md §3.7),且各 hook URL 末尾的?secret=与平台vms.gb28181.zlm.hook-secret(全局 yml 配置,≠ 节点的[api] secret)一致。其中与WAITING→RUNNING直接相关的是on_stream_changed,缺了它会话就永远不转 RUNNING;on_stream_none_reader负责无人观看时主动关流,缺它实时点播流只能等 ZLMstream_idle_timeout兜底。 - 流名归一。ZLM 被动建流时按 RTP 头中 SSRC 的 8 位大写十六进制命名媒体流(如
0200000035→0BEBC223),平台按标准 10 位十进制 SSRC 反查会话。平台已内置canonicalSsrc做 hex→十进制归一;若你看到 ZLM 有流、流名是一串十六进制、而会话仍 WAITING,优先确认on_stream_changed回调是否到达。
# Hook 是否到达平台:看 ZLM 端有没有回调失败(403/超时)
docker logs --since=120s <zlm容器名> 2>&1 | grep -iE "hook|on_stream_changed|on_publish|failed"
# 平台是否把会话置 RUNNING:blade-server 日志
# 出现 "stream ready: ssrc=... ws-flv=..." = 成功;"session missing" = 流名没匹配上会话
# 手动验证 keepalive 回调链路(从容器内打一发,确认 secret/连通/落库)
docker exec <zlm容器名> curl -s -X POST \
"http://host.docker.internal:80/blade-iot/vms/webhook/on_server_keepalive?secret=<hook-secret>" \
-H 'Content-Type: application/json' -d '{"mediaServerId":"<节点编码>"}'拉流地址拉不动还需检查:
publicHost(浏览器侧拉流地址,留空回退节点地址)、httpSslPort(管理端走 HTTPS 时必配,否则混合内容被拦)、internalHttpPort(Docker 映射端口 ≠ 容器内端口时必配,截图等内部回环拉流用)。
4.5 收流口「address already in use」
ZLM 启动时 [rtp_proxy] port 就常驻监听该端口;若平台又在同一端口 openRtpServer,会撞 address already in use。单端口模式下平台不应再 openRtpServer(已内置:单端口分支直接复用常驻收流口、closeRtpServer 为 no-op)。确认媒体节点 rtp_port 与 ZLM [rtp_proxy] port 一致即可。
五、通用排查工具箱
平台 / 数据库
# SIP 端口监听
lsof -nP -iTCP:5060 -sTCP:LISTEN ; lsof -nP -iUDP:5060
# 设备 / 通道 / 会话 / 媒体节点(查库一律带 is_deleted=0,否则会查到软删的旧行误导判断)
docker exec mysql mysql -uroot -proot bladex_iot -e "SELECT device_id,transport,ip,port,online_status FROM iot_vms_device WHERE is_deleted=0\G"
docker exec mysql mysql -uroot -proot bladex_iot -e "SELECT ssrc,stream,session_status FROM iot_vms_stream_session WHERE session_status IN('WAITING','RUNNING')\G"媒体节点(ZLM REST)
B="http://localhost:<HTTP端口>/index/api" ; S="secret=<secret>"
curl -s "$B/getMediaList" --data "$S" # 当前所有流(app/stream/schema/bytesSpeed)
curl -s "$B/listRtpServer" --data "$S" # 多端口模式下动态收流口列表
# 容器内端口监听(hex:10000=2710 / 1935=078F / 80=0050)
docker exec <zlm容器名> sh -c "cat /proc/net/tcp" | awk '$2 ~ /:2710$/{print $0}'
docker logs --since=60s <zlm容器名> 2>&1 | grep -iE "RtpProcess|publish|hook|listen"SIP 抓包(信令疑难必备)
# 需 BPF 权限:在你自己的终端里跑(! 前缀是非交互的,sudo 弹不出密码)
sudo tcpdump -i <nic> -s 0 -w /tmp/sip.pcap "host <设备IP> and port 5060"
# 离线读:看请求行 / 状态行 / SDP
tcpdump -r /tmp/sip.pcap -nn -A | grep -aE "REGISTER|INVITE|MESSAGE|SIP/2.0|^m=video|^a=setup|^y="blade-server 日志关键字
国标设备注册成功 · Catalog query sent · INVITE Play sent ... mediaPort= · stream ready · session missing · 摘要校验失败,已拒绝接入(403)。
关键校验清单
- 查库务必带
is_deleted=0:软删旧行会被误读为"重复记录"。 - ZLM 被动流名是十六进制(
%08X)而非十进制,反查会话前须先归一为十进制。 - tcpdump 需 BPF 权限,
sudo要在真实终端执行。 - 四个 Hook(
on_publish/on_stream_changed/on_stream_none_reader/on_server_keepalive)+ secret 必须配对,缺on_stream_changed会话不会转 RUNNING。 - Docker 单端口只放行一个口、多端口放行整段,收流模式与端口映射必须匹配。
- 会话卡 WAITING 先看
dialog_ok:0=信令未通,1=信令已通则查收流/回调。
