
1. 先把这套组合的职责边界划清楚1.1 信令走 WVP、媒体走 ZLMediaKit为什么不做成一体刚接触 wvp-GB28181-pro 的人最容易犯的错是把 WVP 当成一个流媒体服务器。实际上它压根不碰视频流。整套 GB28181 视频平台在 CentOS 7 上跑起来逻辑上是两台独立服务在配合WVP 负责 SIP 信令设备的注册、心跳、目录查询、INVITE 点播、云台控制、语音广播ZLMediaKit 负责 RTP 收流、解 PS 封装、转协议分发RTSP/RTMP/HLS/HTTP-FLV/WebRTC。两者之间靠两样东西通信WVP 调 ZLM 的 HTTP API比如openRtpServer开收流端口、closeRtpServer关端口ZLM 主动回调 WVP 的 hook 接口on_publish、on_stream_changed、on_rtp_server_timeout等。这个拆分带来的最大好处是故障域隔离。信令出问题只是设备点不开媒体出问题只是画面卡或黑屏。如果你把两者揉在一块儿一次 Java 进程的 Full GC 就会把正在传输的流一起干掉。而且 ZLMediaKit 是 C 写的本身单机撑几千路转发问题不大Java 侧的 WVP 只处理信令CPU 占用极低两者对机器的资源诉求完全不同——这种不对称恰恰说明它们本就该分开部署。我一般建议至少给出一个明确的分工认知WVP 是指挥官它只下达命令、记录状态、维护设备树ZLMediaKit 是搬运工它只负责把 UDP 上来的 RTP 包拆包、重新封装、按需分发给浏览器或第三方播放器。理解了这层后面所有配置项的归属就不会搞混——凡是以sip:开头的参数是给 WVP 的凡是以[rtp]、[rtsp]这种段落结构出现的是给 ZLM 的。还需要提前明确一个概念GB28181 里所有媒体流都是设备推、平台收。不是平台去设备那里拉。设备收到 INVITE 后按 SDP 里协商好的 IP 和端口主动把 PS 流用 RTP 推上来。这一点非常关键后面讲级联和排查黑屏时会反复用到。1.2 版本组合选型先定 JDK再定其余这一步很多人跳过结果编译到一半发现 JDK 版本对不上。WVP-GB28181-pro 从 2.7 开始切换到了 Spring Boot 3要求 JDK 172.6.x 及更早是 Spring Boot 2.xJDK 8 即可。CentOS 7 上默认的 OpenJDK 只有 1.8如果你要跑 2.7得自己装 JDK 17 并改JAVA_HOME。我个人的建议是存量设备多、追求稳妥就上 2.6.x JDK 8社区资料最全踩坑帖子也最多新项目、需要 WebRTC 低延迟播放、需要更规范的接口就上 2.7.x JDK 17。下表是两种组合的实际差异供你决策。对比项WVP 2.6.xWVP 2.7.x运行环境JDK 8 Spring Boot 2.7JDK 17 Spring Boot 3.x数据库MySQL 5.7 / 8.0 均可建议 MySQL 8.0配置结构media节点扁平media下拆出rtp、stream等子节点前端Vue 2 打包后放staticVue 3 独立打包级联稳定性成熟资料多修复了不少级联保活问题ZLMediaKit 这边反而简单它提供了编译好的二进制包不用非得源码编译。CentOS 7 自带的 GCC 是 4.8.5编译新版 ZLMediaKit 会报 C14 相关的错得装devtoolset-8之类的工具链折腾成本不低。除非你要改源码否则直接拿官方 Release 里的 linux 包解压就能用省半天时间。注意CentOS 7 在 2024 年 6 月已经走完官方维护周期默认的mirror.centos.org地址大多失效。装依赖之前先把 yum 源指向归档地址或者干脆换成本地镜像源否则你会在第一步就卡住。1.3 端口清单与带宽估算动手前先算一遍GB28181 平台最典型的故障原因不是代码问题是端口没开。下面这张表建议你部署前对着firewall-cmd --list-ports逐条核对一遍。端口协议归属用途5060UDP/TCPWVPSIP 信令设备注册与点播18080TCPWVP后端 HTTP 服务也是 ZLM 的 hook 回调地址80 / 8090TCPZLMHTTP 拉流、HLS、FLV554TCPZLMRTSP 拉流1935TCPZLMRTMP 拉流/推流8000UDPZLMWebRTC 播放30000-30500UDPZLMRTP 收流端口段带宽一定要提前算。单路 1080P H.264 主码流通常按 4 Mbps 估如果 100 路设备同时点播出口就是 400 Mbps。这个数字不是让你去申请这么多带宽而是提醒你前端页面别一屏开 16 路开四路就够看需要多路监控就上轮巡。媒体端口段的大小也有讲究——30000-30500是 501 个端口决定了同时最多能有多少路流在收。如果你只开 10000 单端口靠 SSRC 复用也能跑但排查问题时流全挤在一个端口上抓包会非常痛苦。我一般按预计并发路数 50% 余量来设端口段。2. CentOS 7 基础环境铺设2.1 系统初始化四件必做的事拿到一台干净的 CentOS 7先做四件事顺序别乱。第一件处理 yum 源。如果系统还能联网但yum install报 404改归档地址sed -i s/^mirrorlist/#mirrorlist/g /etc/yum.repos.d/CentOS-Base.repo sed -i s|^#baseurlhttp://mirror.centos.org|baseurlhttp://vault.centos.org|g /etc/yum.repos.d/CentOS-Base.repo yum clean all yum makecache第二件关掉 SELinux。ZLMediaKit 和 WVP 都涉及大量端口监听和文件读写SELinux 开着会在你看不见的地方拦截。生产环境可以配策略测试环境直接关setenforce 0 sed -i s/^SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config第三件开防火墙端口。CentOS 7 用的是 firewalldRTP 端口段要整段放行firewall-cmd --zonepublic --add-port5060/udp --permanent firewall-cmd --zonepublic --add-port5060/tcp --permanent firewall-cmd --zonepublic --add-port18080/tcp --permanent firewall-cmd --zonepublic --add-port80/tcp --permanent firewall-cmd --zonepublic --add-port554/tcp --permanent firewall-cmd --zonepublic --add-port1935/tcp --permanent firewall-cmd --zonepublic --add-port8000/udp --permanent firewall-cmd --zonepublic --add-port30000-30500/udp --permanent firewall-cmd --reload第四件调大文件句柄和时间同步。ZLMediaKit 每路流都会占若干 fd默认 1024 在几十路并发时就爆了echo * soft nofile 65535 /etc/security/limits.conf echo * hard nofile 65535 /etc/security/limits.conf时间同步也要做SIP 注册的Expires头和心跳全靠时间戳判断服务器时间飘了会出现设备反复掉线。跑一条yum install -y chrony systemctl enable --now chronyd就够。提示这四件事里最容易被忽略的是文件句柄。我遇到过一次平台跑了两小时突然所有流中断查了半天是 ZLM 打不开新 socketulimit -n一查还是 1024改完重启就再没犯过。2.2 Java、MySQL 8、Redis 三件套Java 按前面选定的版本装。如果你走 JDK 17 路线yum install -y java-17-openjdk java-17-openjdk-devel alternatives --config java echo export JAVA_HOME/usr/lib/jvm/java-17-openjdk /etc/profile source /etc/profile java -versionMySQL 8 建议用官方 yum 仓库装别用 CentOS 自带的 MariaDBWVP 的部分 SQL 语法比如JSON字段、utf8mb4_0900_ai_ci排序规则在 MariaDB 上会报错。安装后要改两个地方character_set_serverutf8mb4和default_authentication_pluginmysql_native_password。后者是因为 WVP 用的连接池驱动在某些版本下对caching_sha2_password支持不好会报认证失败。Redis 装完改/etc/redis.conf把bind 127.0.0.1保持不动WVP 和 Redis 同机部署就够设一个requirepass。WVP 用 Redis 存的是流状态缓存和 token不是业务数据丢了不影响设备注册但会导致正在播放的流状态不一致所以别再拿 Redis 当主存储用。这里有个顺序建议先装 MySQL 和 Redis 并确认能本地连上再动 WVP。因为 WVP 启动时会立刻去连数据库和 Redis连不上就是一堆Connection refused刷屏日志噪音会盖掉真正的报错。2.3 依赖包一次性装齐把编译和运行需要的包装齐避免来回补yum install -y epel-release yum install -y gcc gcc-c cmake make git wget unzip \ openssl openssl-devel libsrtp libsrtp-devel \ ffmpeg net-tools tcpdump lsofffmpeg一定装上它不是给平台用的是给你自己验证 ZLM 收流是否正常用的后面会用它推一路测试流来确认媒体通道通不通。tcpdump是排查 SIP 和 RTP 的救命工具没有它你只能靠猜。3. ZLMediaKit 部署与关键配置3.1 二进制包 vs 源码编译官方 Release 页面提供ZLMediaKit_linux_release_xx.tar.gz这类包解压后目录里就是MediaServer可执行文件和一堆配置。这条路在 CentOS 7 上能省掉工具链的坑。如果你确实要源码编译CentOS 7 必须先升 CMake自带的是 2.8最低要求 3.1新版要 3.13和 GCC自带 4.8.5 不支持 C14 部分特性yum install -y centos-release-scl yum install -y devtoolset-8 scl enable devtoolset-8 bash gcc --version然后按官方流程mkdir build cd build cmake .. make -j4。编译过程在 2 核机器上大概要十几分钟4 核会快不少。注意scl enable只在当前 shell 生效。如果你用了screen或nohup在别的会话里编译工具链是没切过去的会报一堆#error This file requires compiler support。3.2 config.ini 里必须改的那几项ZLMediaKit 的配置集中在conf/config.ini。这个文件很长但真正需要动的没几处。下面按段落说明。[general] mediaServerIdzlm-01 flowThreshold100 enableVhost0 [hook] enable1 on_server_started on_publish on_play on_stream_changedhttp://127.0.0.1:18080/index/hook/on_stream_changed on_stream_none_readerhttp://127.0.0.1:18080/index/hook/on_stream_none_reader on_rtp_server_timeouthttp://127.0.0.1:18080/index/hook/on_rtp_server_timeout timeoutSec10 [rtp] port_range30000-30500 [rtc] port8000mediaServerId必须和 WVP 里配的媒体节点 ID 一致否则 WVP 调 API 时 ZLM 会拒绝日志里会出现hook 校验失败之类的字样。on_stream_none_reader这个回调很实用——当一路流没有任何观众时WVP 会收到通知并主动关流避免设备白白推流占用带宽。on_rtp_server_timeout则负责回收那些设备没推流的空端口这个如果不配跑一段时间端口段就会被耗尽表现为新点播全部失败。[rtp] port_range要和 WVP 里配的端口段完全对齐两边不一致会出现 WVP 通知 ZLM 在 30100 开端口、ZLM 却说自己只有 30000-30050 的尴尬局面。3.3 启动后先自检别急着接设备启动 ZLMcd ZLMediaKit ./MediaServer -d 然后看log/目录下最新日志确认三件事HTTP 端口监听成功、RTP 端口段分配成功、hook 回调没有报 404此时 WVP 可能还没起404 正常。接着用 ffmpeg 推一路本地测试流验证 ZLM 的收流和转发链路完整ffmpeg -re -i test.mp4 -c copy -f rtsp rtsp://127.0.0.1:554/live/test换个终端用 ffplay 拉一下ffplay rtsp://127.0.0.1:554/live/test这一步能出画面说明 ZLM 本身没问题后面接 GB28181 出问题就一定是信令或 RTP 参数的事排查范围直接缩小一半。这个先隔离验证的习惯能帮你省掉大量时间很多人一上来就接摄像头黑屏了根本分不清是 ZLM 坏了还是 SIP 没通。4. WVP-GB28181-pro 部署与核心配置4.1 建库、导 SQL、调连接池先在 MySQL 里建库CREATE DATABASE wvp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER wvp% IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON wvp.* TO wvp%; FLUSH PRIVILEGES;然后从 WVP 项目根目录找 SQL 文件导入。不同版本路径不一样2.6.x 一般在数据库/目录下2.7.x 在db/或doc/下文件名通常带mysql或wvp字样。直接mysql -uwvp -p wvp xxx.sql就行。导入完检查表数量正常应该有几十张表wvp_device、wvp_device_channel、wvp_platform这几个是核心。如果只有两三张表说明 SQL 文件选错了你导的可能是升级脚本而不是全量脚本。连接池这块WVP 用 HikariCP。单机接几百台设备的话maximum-pool-size给到 20 就绰绰有余——它只处理信令不像业务系统那样高并发查库。盲目调到 100 反而会因为连接数过多拖慢 MySQL。4.2 application.yml 逐段拆解这是整个部署里最容易出错的地方。下面按 2.6.x 的结构讲2.7.x 的键名有微调以你本地文件为准。server: port: 18080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicodetruecharacterEncodingUTF8serverTimezoneAsia/Shanghai username: wvp password: 你的密码 redis: host: 127.0.0.1 port: 6379 password: 你的redis密码 sip: ip: 192.168.1.100 port: 5060 domain: 3402000000 id: 34020000002000000001 password: 12345678 # 级联时作为下级平台对外暴露的地址 show-ip: 192.168.1.100 media: id: zlm-01 ip: 192.168.1.100 sdp-ip: 192.168.1.100 stream-ip: 192.168.1.100 hook-ip: 192.168.1.100 http-port: 80 secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc rtp-port: 30000几个必须拎出来讲的字段sip.ip是 WVP 监听 SIP 的地址多网卡机器上一定要写内网实际 IP不能写 0.0.0.0否则 SDP 里带出去的地址是错的设备会把流推到错误的地址上。sip.domain是 SIP 域通常用 10 位数字比如3402000000。sip.id是平台自身的 SIP 编号20 位规则是域(10位) 类型编码(4位) 序号(6位)其中平台类型编码固定是2000。所以340200000020000000000134020000002000000001这个编号全网不能重复。media.secret必须和 ZLMconfig.ini里的[general] secret完全一致这是 WVP 调 ZLM API 的凭证。我见过有人只改了一边然后所有点播都返回打开 RTP 服务器失败日志里却只显示 HTTP 401很容易误判成网络问题。media.rtp-port是单端口模式下的收流端口。WVP 支持两种模式单端口所有流都发到 30000靠 SSRC 区分和多端口每路流动态分配一个端口。单端口模式下防火墙规则少但抓包分析困难多端口模式更好排查代价是要放行一整段。提示sdp-ip和stream-ip在 NAT 环境里是最要命的。如果 WVP 部署在云主机、设备在办公网SDP 里带的必须是公网可达地址否则设备收不到正确的收流地址表现为信令 200 OK 但一直没有画面。4.3 打包前端、启动服务、确认 hook 通WVP 的前端通常是独立目录比如web/或ui/用 npm 构建cd web npm install npm run build构建产物拷到后端src/main/resources/static下或者直接用 Nginx 托管。如果只是自己用我建议用 Nginx 反代前端改动不用重新打包后端省事。后端起服务mvn clean package -DskipTests java -jar target/wvp-pro-x.x.x.jar --spring.config.locationapplication.yml启动成功后去 WVP 的媒体节点页面看 ZLM 是否显示在线。这一步是分水岭如果显示离线说明 WVP 调不通 ZLM 的 API检查media.secret、media.ip、media.http-port三项如果显示在线但流状态不刷新说明 ZLM 的 hook 回调打不到 WVP去 ZLM 日志里看回调 URL 有没有 404。两个方向是独立的WVP → ZLM 走 HTTP APIZLM → WVP 走 hook 回调。一个方向通不代表另一个方向通这个认知能帮你精准定位。5. 设备接入与联调实测5.1 摄像头侧参数怎么填在摄像头的平台接入页面通常要填这几项SIP 服务器地址192.168.1.100SIP 服务器端口5060SIP 域3402000000SIP 用户/设备编号20 位比如34020000001320000001注册密码和设备编号对应的密码注册有效期3600秒设备编号的构成规则要记住前 10 位是域中间 4 位是类型后 6 位是序号。常见的类型编码里132是摄像机131是球机118是报警主机。比如34020000001320000013402000000132000000 1这样 20 位。不同厂商的界面差异很大。海康一般叫平台接入 → GB28181大华叫国标28181宇视在网络 → 高级配置里。有个通用技巧填完保存后重启一次设备的网络服务不少型号不重启不生效你会以为配错了。注册成功后在 WVP 的设备页面能看到设备状态变绿点开能展开通道列表。看不到通道通常是两个原因设备的目录查询响应超时或者通道被禁用了。前者等十几秒会自己出来后者要去设备的通道管理里手动启用。5.2 点播、录像、语音对讲点播的完整链路是WVP 收到前端请求 → 调 ZLMopenRtpServer开端口 → 给设备发 INVITESDP 里带上收流 IP 和端口 → 设备回 200 OK → 设备开始推 RTP → ZLM 收到第一个包后通过 hook 通知 WVP → WVP 把播放地址返回前端。这条链路上任何一环断了都会黑屏所以排查时要按顺序看 WVP 日志 → ZLM 日志 → tcpdump。录像功能依赖 ZLM 的record配置。默认是点播即录也可以配成按计划录。录像文件默认落在 ZLM 目录下的www/record/里格式是 MP4。要注意磁盘空间——100 路 24 小时能吃掉几 TB我一般会在 ZLM 配置里加fileSecond3600按小时切片再配合定时任务清理超过 7 天的文件。语音对讲是比较容易被忽略的能力它需要满足三个条件设备本身支持音频通道且麦克风未被禁用SDP 协商里带了音频媒体描述通常是artpmap:8 PCMA/8000ZLM 开启了对应的音频转发。实际调试时如果只有单向能听、反向没声音八成是设备的音频编码和平台协商的不一致把 SDP 里的maudio那行的端口和 payload 号对一遍基本能定位。另外注意很多球机的对讲通道和视频通道是分开编号的点播用的是视频通道对讲得切到音频通道上去发 INVITE。5.3 级联媒体流方向这个坑必须先说清关于热词里提到的wvp-gb28181-pro 不支持上级平台主动向下级联拉取资源这里需要澄清一个概念GB28181 的级联在设计上就不存在上级去下级拉流这回事。级联中的媒体流方向永远是下级主动向上级推送。上级发 INVITE 给下级下级收到后按 SDP 里的地址把流推到上级的收流端口上方向是下级 → 上级。所以当你在 WVP 里配级联遇到上级平台点播下级资源失败问题不在能不能拉而在下面几个点上第一SIP ID 冲突。上级和下级如果用了同一个域或者同一个平台 IDSIP 消息会互相打架。级联的两端必须是不同的域比如上级3402000000、下级3402010000。第二网络可达性。上级要能访问下级的 SIP 端口5060下级也要能访问上级的 SIP 端口和 RTP 收流端口段。这两个方向是独立的只开一边不够。第三Catalog 查询响应。上级发Catalog查询下级资源下级必须在超时前返回完整通道列表。如果下级设备多、通道上千一次性返回可能超过 UDP 的 MTU 被分片丢弃这时要开 WVP 里的目录分批返回选项。第四媒体流推送目标地址。下级推流时用的地址来自上级 INVITE 的 SDP。如果上级在 NAT 后面SDP 里的地址是内网 IP下级推过去必然失败。这种情况需要在上级配置里显式指定stream-ip为公网地址。级联症状大概率原因验证方法上级看不到下级设备SIP ID 或域冲突两端日志对比 SIP 消息中的 From/To 域上级看到设备但点播失败RTP 端口未放行在上级机器 tcpdump 看是否有包到达点播成功但立即断开下级推流地址不对抓下级出口流量看目标 IP目录只有部分通道单包过大被分片丢弃开启分批返回6. 踩坑速查与排查手法6.1 常见问题速查表下面这张表是我几次部署里攒下来的高频问题按症状 → 直接原因 → 处理的形式整理出问题时可以当索引查。症状直接原因处理方式设备一直显示未注册SIP 域或编号不匹配核对设备侧域与 WVPsip.domain注册成功但无通道目录查询超时检查设备目录查询开关等待重试媒体节点显示离线secret 或端口不一致两侧比对media.secret和http-port点播返回 200 但黑屏RTP 端口未放行或 IP 错误tcpdump 抓 30000 段 UDP 包流播几秒后断开无观众自动关流检查on_stream_none_reader配置新点播全部失败RTP 端口段耗尽检查on_rtp_server_timeout是否生效平台跑一段时间卡顿文件句柄耗尽ulimit -n调到 65535级联后设备重复出现上下级域相同改成不同的 SIP 域6.2 抓包与日志的三层定位法出问题时不要凭感觉改配置按这三层往下走。第一层看 WVP 日志。WVP 用的是 Logback日志里 SIP 消息会有明确的收发标记。找INVITE、200 OK、ACK这几个关键字看信令走到哪一步断了。如果连 INVITE 都没发出去问题在 WVP 内部可能是 Redis 连不上导致流状态拿不到。第二层看 ZLM 日志。ZLM 的日志在log/目录下会记录 RTP 收流的开始和结束以及 hook 回调的返回码。看到openRtpServer成功但一直没有on_stream_changed说明设备根本没推流过来问题在设备侧或网络侧。第三层抓包。这一步才是确定性的证据# 抓 SIP 信令 tcpdump -i eth0 -n port 5060 -w sip.pcap # 抓 RTP 收流 tcpdump -i eth0 -n udp portrange 30000-30500 -w rtp.pcap拿sip.pcap用 Wireshark 打开看 INVITE 里的 SDP 内容重点看c那行的连接地址和mvideo那行的端口。这两个值就是设备要推流的目标一旦不对后面全白搭。rtp.pcap里如果全是包说明网络通、是平台侧处理有问题如果一个包都没有说明设备没推或防火墙拦了。提示抓 RTP 包时用-c 100限制包数不然 4 Mbps 的流抓几分钟就是几个 G磁盘瞬间爆掉。6.3 几个能提高稳定性的调优点跑起来和跑得稳是两回事下面几个点是我在实际运行中调出来的。ZLM 的线程数。config.ini里有[general] threadNum默认是 CPU 核数。如果主要是转发不加转码保持默认就行如果开了 HLS 切片和 MP4 录制可以适当加 2-4 个但别翻倍线程太多调度开销反而拖慢转发。WVP 的 JVM 参数。只做信令的话-Xms512m -Xmx1g足够跑几百台设备。设成 4G 是浪费还会让 GC 停顿变长。加上-XX:UseG1GC会更平滑。RTP 端口段的回收。前面提过on_rtp_server_timeout这个一定要配。我遇到过跑了两天后所有点播都失败的场景查了半天是设备异常离线没发 BYEWVP 没释放端口501 个端口被慢慢占满。配好超时回收之后这个问题就没再出现。录像磁盘的独立挂载。录像 IO 是持续的写入如果和系统盘共用MySQL 的写入延迟会被拖高表现为偶尔的信令超时。有条件就单独挂一块盘给www/record/。设备心跳间隔。默认 60 秒设备多的时候会形成心跳风暴几百台设备同一秒发注册刷新SIP 线程会被瞬间打满。把过期时间设长一点让设备的心跳自然错开比加机器有效。最后分享一个我自己用了很久的小习惯部署完先在本地用 ffmpeg 推一路测试流跑通全链路再接入真实设备。因为 ffmpeg 的行为完全可控而摄像头的实现千奇百怪。当你知道平台侧一定没问题时任何一个报错都能精准指向设备排查效率至少翻一倍。这个顺序反过来做你会同时面对两个未知那是纯粹的时间黑洞。