GB28181 这套东西,第一次接触的人大概都会被"国标"两个字唬住,觉得是运营商的活儿,实际上把它拆开看,无非是一个 SIP 信令 + RTP 媒体流的组合,再配一套前端管理和一个流媒体转发服务。我自己第一次在 CentOS 7 上搭 wvp-GB28181-pro 加 ZLMediaKit 的时候,前后折腾了两天,踩的坑主要不在代码本身,而在依赖版本、端口占用和配置文件里几个不起眼的字段。这篇就把整个部署流程从环境准备到联调验收完整走一遍,顺带把那些文档里往往一笔带过、但实际会让服务起不来的细节讲清楚。适合有一定 Linux 基础、想自建国标视频接入平台的运维和开发看,新手照着做也能跑通,老手可以重点看第四章和第五章的配置字段与排查思路。
1. 先把组件分工理清:为什么是 WVP 加 ZLMediaKit 这套组合
很多人上手第一步是把两个项目都 clone 下来,结果发现配置文件里到处都是对方的地址,互相看不懂。根源在于没弄清这两个服务各自负责什么。国标协议规定了设备怎么注册、目录怎么查询、信令怎么交互,但它没规定视频流具体由谁来转发。WVP 负责的是"信令大脑",ZLMediaKit 负责的是"媒体搬运工",两者通过 HTTP Hook 和一串约定好的地址通信。
1.1 WVP 管信令,ZLMediaKit 管流
wvp-GB28181-pro 是基于 Spring Boot 的 Java 服务,它本身收发 SIP 消息:设备向它注册,它向设备发起 INVITE 邀约,设备把 RTP 流推给它指定的地址。但 WVP 自己并不擅长转发大流量媒体流,所以它把"收流地址"交给 ZLMediaKit。
具体流程是这样的:设备注册到 WVP 之后,用户在网页上点击"播放",WVP 向设备发送 INVITE,SDP 里写的接收地址其实是 ZLMediaKit 的 IP 和端口。设备把 RTP 推过来,ZLMediaKit 收流并转成 RTSP、HTTP-FLV、HLS、WebRTC 等格式,前端播放器再从 ZLMediaKit 拉流。WVP 通过配置的 hook 地址监听 ZLMediaKit 的事件(比如"流注册成功""流无人观看"),从而知道流什么时候该关。
理解这一层之后,配置里那些media.ip、media.stream-ip、hook地址就不会填错了——它们指向的都是 ZLMediaKit 那一端,而不是 WVP 自己。
1.2 部署顺序为什么建议先 ZLMediaKit 后 WVP
我试过反过来,先起 WVP 再装 ZLMediaKit,结果是 WVP 启动时尝试连 hook 地址失败,日志里一堆连接拒绝,虽然服务不会崩,但排查起来干扰很大。
ZLM 是底层依赖,先把它跑起来并且用curl确认 API 可访问,再启动 WVP,WVP 一启动就能连上,日志干净。这个顺序的好处在于,出问题时你能明确知道是哪一层的锅,而不是两个服务都没起来互相甩锅。
1.3 CentOS 7 上的版本选择要点
CentOS 7 自带的软件版本比较老,这是后面一堆编译问题的根源。几个关键点先摆出来:
| 组件 | CentOS 7 默认版本 | 建议版本 | 说明 |
|---|---|---|---|
| GCC | 4.8.5 | 8 及以上 | ZLM 编译依赖 C++17 |
| CMake | 2.8.12 | 3.1 以上 | 低于 3.1 无法配置 ZLM |
| JDK | 通常无 | 8 或 11 | 以 WVP 版本要求为准 |
| MySQL | 无 | 5.7 或 8.0 | 注意字符集配置 |
| Redis | 无 | 5 以上 | WVP 缓存与状态存储 |
提示:ZLM 的编译工具链用
devtoolset系列升级最省事,不必冒险替换系统 GCC,避免影响其他已编译软件。
这一章的目的是让你心里有张地图。接下来进入真正的动手环节,从系统环境开始收拾。
2. CentOS 7 系统层的准备工作:把地基打平
系统环境这一步最容易被跳过,但后面 80% 的"服务起不来"都能追溯到这里。CentOS 7 默认的防火墙、SELinux、时钟、句柄数,都可能成为拦路虎。我一般会花十几分钟把这几项一次性处理掉,后面就省心了。
2.1 关掉 SELinux 和调整防火墙
SELinux 在容器化部署里经常是隐藏的坑,尤其是在 /usr/local 下自建目录、或者让 Nginx 反代本地端口的时候,会出现"权限明明对但就是访问不了"的诡异现象。图省事的做法是直接设成 permissive 模式:
# 临时生效 setenforce 0 # 永久生效 sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config防火墙方面,国标部署涉及的端口挺多,与其一个个记,不如先统一放开内网,或者按下面这张表逐条放行。生产环境当然要精细化,测试环境可以先把 firewalld 停掉减少干扰:
systemctl stop firewalld systemctl disable firewalld如果必须保留防火墙,需要放行的端口大致如下:
| 端口 | 协议 | 用途 |
|---|---|---|
| 8116 | UDP/TCP | WVP 的 SIP 端口 |
| 5060 | UDP/TCP | 部分场景下的标准 SIP 端口 |
| 18080 | TCP | WVP 的 Web 与 API |
| 80 | TCP | ZLM 的 HTTP 与 FLV/HLS |
| 554 | TCP | ZLM 的 RTSP |
| 1935 | TCP | ZLM 的 RTMP |
| 10000 起 | UDP | ZLM 收 RTP 流的端口段 |
| 30000 起 | UDP | 发送 RTP 流的端口段 |
注意:RTP 收流端口段和发流端口段一定要和后面配置文件里的
port-range对得上,范围对不上设备推流会直接超时,这是非常隐蔽的一类故障。
2.2 时钟同步与句柄数调优
国标信令里带时间戳,如果服务器时间和设备时间偏差太大,个别设备会拒绝注册或者注册后立刻掉线。CentOS 7 装好后先确认时间:
timedatectl status # 若未同步,启用时间同步 timedatectl set-timezone Asia/Shanghai句柄数这块,媒体服务在高并发收流时打开的文件描述符相当可观,默认的 1024 往往不够。修改/etc/security/limits.conf,追加:
* soft nofile 65535 * hard nofile 65535改完之后要重新登录终端才生效,用ulimit -n确认。这一项在做压测或者接入几十路以上设备时尤其重要,否则会看到 ZLM 报 "too many open files" 然后无故断流。
2.3 编译工具链的安装
ZLM 需要 CMake 3.1 以上和较新的 GCC。CentOS 7 上装 devtoolset-8 是常用做法:
yum install -y centos-release-scl yum install -y devtoolset-8-gcc devtoolset-8-gcc-c++ scl enable devtoolset-8 bashscl enable只在当前会话生效,装完编译完就可以了。如果嫌每次都要敲,可以写进/etc/profile.d/里自动加载。
CMake 版本不够的话,直接从官网下个二进制包解压即可,不用编译:
cd /usr/local wget https://cmake.org/files/v3.20/cmake-3.20.0-linux-x86_64.tar.gz tar -zxvf cmake-3.20.0-linux-x86_64.tar.gz ln -s /usr/local/cmake-3.20.0-linux-x86_64/bin/cmake /usr/bin/cmake这些准备工作看着琐碎,但每一项都对应后面一个潜在的坑。地基打平之后,编译和部署就顺多了。
3. 让 ZLMediaKit 先跑起来:编译、配置与自检
ZLMediaKit 是整个平台的媒体中枢,它的状态直接决定了画面能不能出来。这一章我会把编译方式的选择、配置文件里必须改的项、以及如何确认它真的健康都讲一遍。跑通这一章,后面 WVP 的联调会轻松很多。
3.1 编译方式怎么选
ZLM 官方提供了源码编译和预编译包两条路。我的经验是:如果只是测试,可以直接用带linux-x86_64字样的预编译包,解压就能跑;但生产环境还是建议源码编译,一来方便后续加功能,二来能避开预编译包对某些系统库版本的隐性依赖。
源码编译的大致流程:
git clone --depth 1 https://github.com/ZLMediaKit/ZLMediaKit.git cd ZLMediaKit git submodule update --init mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4编译耗时不短,四核机器大概十几分钟。完成后release/linux/Release/目录下会有MediaServer可执行文件。
有个细节值得说:cmake如果不带任何参数,默认会尝试构建测试用例,可能因为缺少一些可选依赖报错。加-DENABLE_TESTS=OFF能规避这类问题。编译失败时先看错误发生在哪个模块,多半是某个子模块没拉全,重新git submodule update --init再编译即可。
3.2 config.ini 里必须改的几项
ZLM 第一次运行会在当前目录生成config.ini。这个文件很长,但真正需要动的不多。下面这几项不改,WVP 大概率连不上:
[api] # API 的密钥,WVP 里要填一致 secret=你的随机密钥 [general] # 媒体服务器唯一标识,WVP 靠它区分实例 mediaServerId=你的标识 [http] # HTTP 端口,前端拉 FLV/HLS 用 port=80 # 允许跨域,前端播放器常需要 allow_cross_domains=1 [rtp] # RTP 收流端口段,必须与 WVP 配置对应 port=10000 # 收流超时,设备迟迟不推流就释放 timeoutSec=15 [hook] # 开启 hook 才能让 WVP 感知流事件 enable=1 # 指向 WVP 的地址 on_publish=http://127.0.0.1:18080/index/hook/on_publish on_play=http://127.0.0.1:18080/index/hook/on_play on_stream_changed=http://127.0.0.1:18080/index/hook/on_stream_changed # hook 鉴权参数,要与 WVP 端一致 admin_params=secret=你的hook密钥secret和admin_params这两处是重灾区。很多人改了secret忘了改 hook 里的admin_params,导致 ZLM 调 WVP 的 hook 时被拒绝,表现为"流上了但 WVP 显示离线",排查半天。
提示:
port段和send-port-range段落要一起规划。收流和发流用不同的端口段,可以避免端口冲突,方便在 iptables 里分别做策略。
3.3 用 API 自检确认服务真的活着
服务启动后,别急着配 WVP,先用 API 确认 ZLM 本身没问题。启动命令:
cd release/linux/Release ./MediaServer -d &带-d是后台运行。然后 curl 一下:
curl "http://127.0.0.1/index/api/getServerConfig?secret=你的随机密钥"能返回一大段 JSON,说明 API 正常。再查一下线程和媒体列表:
curl "http://127.0.0.1/index/api/getStatistic?secret=你的随机密钥"如果 API 报 "secret 错误",说明密钥不对;如果直接连不上,检查端口是否被占用、进程是否真的起来了。这一步确认通过之后,再进入 WVP 的部署,心里就有底了。
4. WVP-GB28181-pro 落地:数据库、配置字段与启动
WVP 是 Java 服务,配置项多,但真正需要改的字段集中在application.yml里。这一章我会把数据库准备、关键字段逐个拆解,以及启动时怎么通过日志判断成败。DS 层配置错了,WVP 根本起不来;SIP 层配错了,设备注册不上;media 层配错了,画面出不来。三个层面的问题定位方式完全不同。
4.1 数据库与缓存的准备
WVP 需要 MySQL 和 Redis。MySQL 建库时字符集务必用utf8mb4,否则设备名称里的特殊字符会出问题:
CREATE DATABASE wvp CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;建好库之后,WVP 启动时通常会自动建表(部分版本通过 Flyway 执行迁移脚本)。Redis 主要用来存 SIP 会话状态和临时缓存,默认连本机 6379 即可,如果有密码记得在第 4.2 节的配置里填。
一个容易忽略的点是 MySQL 8 的驱动和时区。CentOS 7 上如果用 MySQL 8,连接串里要带时区参数,否则 WVP 启动时会报时区识别失败:
url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai4.2 application.yml 关键字段逐个拆
这份配置里,我把它分成三段来看。第一段是数据库,第二段是 SIP,第三段是 media。下面只列要点:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai username: root password: 你的密码 redis: host: 127.0.0.1 port: 6379 password: sip: # WVP 监听 SIP 的地址,公网部署要写内网可达 IP ip: 你的服务器IP port: 8116 # SIP 域,一般与设备端配置的 SIP 域一致 domain: 3402000000 # 平台自身国标编号 id: 34020000002000000001 password: 平台注册密码 media: # ZLM 的 ID,要和 config.ini 里 mediaServerId 一致 id: 你的标识 # ZLM 的 IP ip: 你的服务器IP # 流媒体对外发布的 IP,供前端和外部设备访问 stream-ip: 你的服务器IP # SDP 中告知设备的收流 IP sdp-ip: 你的服务器IP # hook 地址,指向 WVP 自己 hook-ip: 127.0.0.1 rtp: enable: true # 收流端口段,要和 config.ini 的 rtp.port 对应 port-range: 10000,10500 send-port-range: 30000,30500sip.ip和media.ip是最容易填混的两处。前者是 WVP 自己收 SIP 的地址,后者是 ZLM 的地址。如果 WVP 和 ZLM 在同一台机器,两者一样;如果分开部署,就要各填各的。
sdp-ip指的是设备推流的目标 IP,也就是设备要往哪个地址发 RTP。这个字段如果填成了内网地址而设备在外网,设备就会推到一个它无法到达的地址,表现为"信令成功但一直没画面"。
4.3 启动顺序与日志判读
配置改完后,启动顺序是 Redis、MySQL、ZLM、WVP。WVP 的启动用 java 命令或打包后的脚本:
java -jar wvp-pro-xxx.jar --spring.config.location=application.yml启动日志里,重点看三行:一是 Spring 容器是否正常启动完成,二是 SIP 监听端口是否成功绑定,三是是否成功连上 ZLM 的 API。看到类似"媒体服务器连接成功"的字样,就说明后端链路通了。
如果日志里出现 hook 连接失败,八成是 ZLM 没起或者端口不对;如果出现数据库连接失败,检查账号密码和网络;如果 SIP 端口绑定失败,多半是端口被占用。这三类问题覆盖了绝大多数启动失败的情况。
4.4 前端访问与初始账号
WVP 起来了之后,浏览器访问http://服务器IP:18080,默认账号密码通常是admin/admin(以你使用的版本为准)。第一次登录建议立刻改密码,因为平台管理权限很大。
登录后进入平台配置页,重点核对"媒体服务器"列表里是否已经出现 ZLM 记录,状态是不是在线。如果显示离线,回到第 3.3 节的 API 自检再确认一遍,多数是secret或 IP 填错。
到这里,后台环境就算搭好了,接下来是最有成就感的环节:让真实设备接进来。
5. 联调验收:设备注册、点播与常见故障定位
环境搭好了,但"服务能跑"和"设备能接"是两回事。这一章聚焦联调,把设备接入的完整链路、点播失败时的排查顺序、以及语音对讲和级联这两个进阶场景讲清楚。这部分是我踩坑最多的地方,也是文档里讲得最少的地方。
5.1 设备注册链路的排查方法
设备注册本质上是一次 SIP REGISTER 交互。设备向 WVP 的 SIP 地址发送注册请求,WVP 校验编号和密码后返回响应。判断注册成功与否,最直接的是看 WVP 的"设备管理"列表里设备状态是否在线。
如果设备一直不在线,按这个顺序查:
- 设备端配置的 SIP 服务器地址是不是 WVP 所在服务器的可达 IP。
- 设备端的 SIP 端口是不是 8116(或你配置的端口),传输协议 UDP 还是 TCP 要和 WVP 一致。
- 设备端的 SIP 域、编号、密码是不是和 WVP 里添加的设备信息完全一致,注意编号位数。
- 服务器防火墙有没有放行 8116。
- WVP 日志里有没有收到 REGISTER 消息。
我遇到过一次,设备端填的 SIP 域少了一位,导致 WVP 收到注册请求但校验失败,日志里能看到请求进来但被拒绝。这种问题不看日志很难定位,所以在排查注册问题时,养成先看 WVP 日志的习惯。
5.2 点播失败时按这个顺序查
点播失败的表现是页面黑屏或者一直转圈,原因可能出现在链路的任何一环。我总结的排查顺序是从后往前:先看 ZLM 有没有收到流,再看 WVP 有没有发出 INVITE,最后看设备有没有响应。
| 排查点 | 观察方式 | 常见原因 |
|---|---|---|
| ZLM 是否收流 | 查 ZLM API 的媒体列表 | 收流端口段不对 |
| WVP 是否发 INVITE | 看 WVP 日志 | 设备编号或域配置错 |
| 设备是否响应 | 抓包或看设备日志 | 设备不支持该编码 |
| 前端能否拉流 | 直接访问 ZLM 的流地址 | 跨域或端口未放行 |
一个很典型的坑是编码协商。有些设备默认推 H.265,而浏览器播放器只支持 H.264,结果就是信令全成功,ZLM 也收到了流,但前端播不出来。这时候要么在设备端把编码改成 H.264,要么在前端使用支持 H.265 的播放方案。定位这类问题,直接拿ffplay或 VLC 去拉 ZLM 的 RTSP 地址,能播说明是前端问题,不能播说明是流本身的问题。
注意:拉流地址里通常带
stream参数,格式随 WVP 版本变化,最稳妥的方法是打开浏览器开发者工具,看播放器实际请求的 URL,照着它去用 ffplay 验证。
5.3 语音对讲与级联场景的注意点
语音对讲(broadcast/talk)和级联是国标平台常见的进阶需求,但它们的信令流程比点播复杂,出问题时也更隐蔽。
语音对讲的方向和点播相反:点播是平台拉设备的流,对讲是平台把音频推给设备。它需要 ZLM 的 RTP 发流端口段可用,同时设备要支持对应的音频编码(通常是 G.711)。如果对讲按下去没声音,先确认设备的对讲功能是否开启,再检查发流端口段有没有被防火墙挡住。
级联方面,常见的需求是下级平台向上级平台注册,然后上级平台向下级查询目录、拉取资源。要注意的是,"上级主动向下级拉取资源"这种能力在不同版本上的支持程度不一样。有些版本需要下级平台主动上报目录,上级才能看到资源。如果你在做这种场景,建议先确认两端平台的版本和级联配置,把catalog订阅和响应的日志都打开,看目录请求是否发出、响应是否返回。这类问题的排查基本靠日志,光看界面状态是不够的。
联调通过之后,平台就算能用了。但要让它稳定跑下去,还有几件运维上的事要做。
6. 上线之后的稳定性维护:那些跑一段时间才暴露的问题
很多人搭好之后觉得万事大吉,结果运行一两周开始出现断流、内存上涨、注册掉线。这一章讲讲长期运行的维护经验,这部分内容基本不会写在部署文档里,但恰恰是生产环境的真实痛点。
6.1 内存、句柄与端口回收
ZLM 长时间运行,如果收流句柄没有及时释放,内存会缓慢上涨。config.ini 里的timeoutSec和流无人观看自动关闭的策略很关键。建议开启"无人观看自动关流",避免僵尸流占着资源:
[hook] # 流无人观看时通知并关闭,减轻服务器负担 on_stream_none_reader=http://127.0.0.1:18080/index/hook/on_stream_none_readerWVP 端也要配置对应的无人观看处理逻辑。另外,RTP 收流端口是有限的,如果某个流异常退出而端口没释放,后续流就会因为端口被占而收不到。定期检查 ZLM 的媒体列表,清理那些没有读者的僵尸流,是个好习惯。
句柄方面,前面调过nofile之后,还要配合监控。用一条简单的命令就能看进程打开了多少描述符:
ls /proc/$(pgrep MediaServer)/fd | wc -l如果这个数字持续增长不降,说明有资源没释放,需要进一步排查是哪个模块的问题。
6.2 升级与配置备份策略
WVP 和 ZLM 都在活跃迭代,升级是免不了的。我的做法是升级前先把配置文件单独备份,因为版本更新经常会调整配置字段,直接覆盖新包可能把配置带跑。备份至少包括application.yml、config.ini和数据库。数据库可以用mysqldump:
mysqldump -uroot -p wvp > wvp_backup_$(date +%F).sql升级 ZLM 时,编译产物和配置文件分开管理,用软链接指向当前版本,回滚时改个软链接就行,不用重新编译。这套方式在多实例部署时尤其好用。
6.3 日常巡检的几个检查项
最后列几个我日常会看的项,形成习惯之后能提前发现大部分问题:
- ZLM 的 API 是否正常响应,媒体列表是否出现异常增多的流。
- WVP 的日志里是否有大量注册失败或者 hook 超时。
- 磁盘空间,尤其是 HLS 切片和日志目录,容易被写满。
- Redis 内存占用,会话状态堆积会导致内存上涨。
- 服务器时间是否仍然同步,时钟漂移会引发信令异常。
把这些做成一个简单的巡检脚本,每天跑一次,比出事之后再救火从容得多。我在实际运维中最大的体会就是,国标平台这类服务,真正难的从来不是第一次搭起来,而是让它连续几个月不出幺蛾子——而后者靠的就是这些看似不起眼的日常维护动作。