Docker部署WVP-Pro实现GB/T 28181视频监控接入实战 做视频监控接入这件事最让人头疼的往往不是摄像头本身而是那个“国标对接”的门槛。GB/T 28181协议在国内视频监控平台上几乎无处不在但真要自己从头去实现一个信令网关加流媒体转发服务光是SIP会话的调试就能熬掉几个通宵。WVP-Pro正是为了解决这个问题而生的开源平台——它封装了国标信令接入、设备管理、流媒体分发配合ZLMediaKit做视频流转发用一套Docker编排就能把整个监控接入层跑起来。这篇文章我会从零开始完整拆解用Docker部署WVP-Pro的流程包括架构设计、配置要点、设备接入实操以及我实际踩过的坑和排查思路。适合正在做安防平台集成、视频Web化、园区监控上云的开发者参考。1. 整体设计思路WVP-Pro和ZLMediaKit是怎么配合工作的先理清楚WVP-Pro在整个视频链路里的位置。它不是一个视频流媒体服务器而是一个信令管理与业务管理平台。设备接入时走的是GB/T 28181的SIP信令——设备把SIP注册消息发到WVP的SIP服务器端口WVP维护设备在线状态、目录列表并下发实时点播指令。真正接收摄像头推过来的RTP视频流、转码封装成HLS/HTTP-FLV/RTSP等格式的是ZLMediaKit这个流媒体引擎。所以Docker部署的最小拓扑包含四类角色WVP-Pro主程序负责SIP信令、设备管理、权限控制、API接口、前端Web界面。ZLMediaKit负责接收设备通过国标协议推上来的音视频流提供拉流出口。MySQL保存设备信息、通道信息、用户账号、操作日志。Redis做缓存、会话管理、部分实时状态存储。用Docker部署有一个很明显的收益WVP-Pro和ZLMediaKit这两套服务有各自独立的依赖环境手动安装时经常因为JDK版本、编译依赖、配置文件路径的问题互相干扰。Docker把它们各自封装成镜像配合Compose编排一次性拉起环境一致性和迁移方便程度都好了很多。但部署这套东西不能只考虑“能启动”。你要理解业务的几个关键链路才能把容器参数配对设备注册链路摄像头/视频平台把SIP注册请求发到WVP的SIP端口WVP响应注册并保存设备信息。点播推流链路用户从Web端点播某通道WVP发送INVITE信令给设备设备收到后用INVITE消息里携带的ReceiveIP和ReceivePort把RTP流推到ZLMediaKit的RTP接收端口。出流链路ZLMediaKit内部完成RTP解封装转出RTSP/RTMP/HLS/HTTP-FLV流浏览器或播放器可从WVP前端获取播放地址。理解了这三条链路你配置容器网络、端口映射、防火墙规则的时候心里就有数了哪些链路是容器内部互相访问哪些是从外部设备直接访问到宿主机端口。2. 容器网络与端口规划决定部署成败的关键阶段很多人在Docker部署时第一步就在网络模式上栽了跟头。WVP-Pro的典型部署场景是服务器有内网IP摄像头也在同一个内网里Web端用户通过内网或公网访问平台。这个场景下我强烈建议ZLM和WVP的SIP端口使用host网络模式而MySQL和Redis用默认的bridge网络即可。为什么不推荐把所有容器都放bridge网络再做端口映射因为国标设备SIP注册有“地址确认”的行为。设备INVITE之后ZLM要告诉设备往哪个IP地址和端口推RTP流——这个信息来自ZLM收到的SIP消息里的网络参数。如果ZLM跑在bridge网络容器自身看到的IP是172.x的内部地址SIP信令里携带的地址就可能是错的摄像头会把视频流推到内网虚拟IP上结果就是设备“在线但不出流”。虽然可以通过强制指定NAT地址让ZLM替换IP字段但这等于额外引入一层NAT转换排查问题的时候非常绕。当前我的推荐是WVP主程序使用host模式SIP端口直接绑定宿主机的UDP/TCP 5060保证SIP信令交互时源地址正确。ZLMediaKit容器也使用host模式它的RTP接收端口范围、HTTP端口、RTSP端口都直接暴露方便设备和播放端直接访问。MySQL与Redis用bridge模式仅容器间内部通信6379和3306端口可以不映射到宿主机避免多余暴露面。对应Docker Compose中就是services: zlmediakit: network_mode: host wvp-pro: network_mode: host depends_on: - mysql - redis如果要控制端口映射而不想用host那就必须把WVP配置中的ip、ZLM配置中的bindIp、nat.ip全系列设置清楚并且保证摄像头能路由到宿主机的映射端口。不是不行但对一个需要快速落地的项目来说多一个变量就多一个坑。Docker部署的初衷是省事没必要给自己增加复杂度和排查成本。端口规划方面下面这几个值要提前定好最好形成一个端口规划表写进运维文档服务端口协议用途WVP SIP信令端口5060UDP/TCP设备国标注册与信令交互WVP媒体端口配合ZLM30000-30500UDP摄像头RTP推流接收范围WVP用户端Web端口8080TCPWeb管理界面访问ZLM HTTP端口80TCP供WVP调用RESTful API拉流ZLM RTSP端口554TCPRTSP出流ZLM RTMP端口1935TCPRTMP出流ZLM HLS/FLV相关8080TCPHLS播放、HTTP-FLV播放如果你所在环境里5060端口被其他服务占了在WVP的配置里可以改成5080但注意设备的国标配置里也要同步改这二者必须一致。3. 核心配置拆解application.yml里那些容易配错的参数WVP的配置都在application.yml里。Docker镜像启动后会从指定目录读取配置文件建议把它放到宿主机的持久化目录中用-v参数挂载进去。我拆几个最重要的配置项按“不配好就肯定跑不通”和“配错但不容易发现”两个层面来讲。首先数据库和Redis连接信息。这部分相对简单spring: datasource: url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicodetruecharacterEncodingUTF8serverTimezoneAsia/ShanghaiuseSSLfalse username: wvp password: wvp123 redis: host: 127.0.0.1 port: 6379 password: database: 0这里要注意serverTimezone必须显式设置。MySQL容器默认时区可能跟宿主机不一致如果不设WVP在记录设备在线时间、通道更新时间时会产生8小时偏移。表面上不报错但后来的判断逻辑全错。其次SIP相关的参数。sip: ip: 192.0.2.10 port: 5060 domain: 3402000000 id: 34020000002000000001 password: 12345678id这串就是平台自己的国标编码前8位是行政区划编码第11到13位是类型编码这里最好统一用200表示国标平台。password建议改掉默认值很多默认部署被扫描软件扫到然后被注入非法设备的案例都是因为密码太简单。domain字段是域编码很多地方叫“国标域”它与id编码规则中前10位保持对应。如果你在接上级平台上级平台会要求你用这个ID在网上注册务必保证跟批复文件一致。然后是WVP与ZLM对接的参数。media: ip: 192.0.2.10 port: 8080 secret: ZLM_SECRET hookKey: ZLM_HOOK_KEY这两项极其关键。WVP会通过HTTP调用ZLM的RESTful接口来创建拉流代理、查询流状态secret值必须和ZLM配置里的secret一致否则ZLM会返回401。同理ZLM通过hook回调通知WVP流已经上线或者无人观看时自动关闭hookKey不一致会导致回调被拒绝。排查问题时很多“流状态不同步”“点播之后马上断流”其实都是这个密钥不匹配导致的。最后关于media配置里RTP接收端口范围。media: rtp: enable: true portRange: 30000-30500这组UDP端口要确保在防火墙中放行。我见过有人全部配置都对设备也注册上了但一点播就黑屏最后发现是云安全组的UDP端口范围没开RTP包根本进不了ZLM。这个端口范围建议至少预留500个因为设备每路实时预览都会占用一对UDP端口做RTP收发摄像头数量多时端口数量不够会出现“部分通道点播失败”的情况。4. 实操部署流程从镜像准备到第一台设备接入环境假设你已经有一台Linux服务器装好了Docker和Docker Compose插件内核版本不低于5.0。下面按步骤走每步我都会说明为什么要这么操作以及成功/失败的直接判断标准。4.1 镜像获取与版本匹配WVP-Pro官方在镜像仓库里维护了两种镜像一种是不带ZLM的纯WVP另一种是集成方案把ZLM包装在同一个镜像里。我的建议是用分离式镜像单独拉WVP和ZLM版本控制更清晰升级其中一个服务不会误伤另一边。docker pull registry.cn-hangzhou.aliyuncs.com/.....注意不同源头的镜像大小相差很大。有的镜像把前端静态文件也一起打进去了启动后访问8080端口直接能看到Web界面有的精简镜像需要你单独挂载前端dist目录。拉下来后先看镜像说明或运行docker inspect查看环境变量确定它是否包含前端页面。版本匹配上尽量保持WVP大版本和ZLM版本的兼容性。特别提醒如果你的WVP版本较新但ZLM镜像过旧hook回调格式不兼容会导致ZLM推送事件到WVP失败表现为“设备离线但流还在”。4.2 初始化数据库WVP的数据库表结构可以由程序自动创建但初始数据比如admin账号、默认角色需要执行初始化SQL脚本。启动流程是这样的第一次在MySQL中创建数据库wvp然后用WVP镜像里的sql/目录下的脚本导入初始数据。如果你用Docker方式跑MySQL可以做如下初始化docker exec -i mysql-container mysql -uroot -p密码 sql/初始化脚本.sql表结构设计上WVP把设备信息、通道信息、设备通道关联关系、用户角色权限分了几张表。设备接入后在数据库的wvp_device表中能看到设备ID、设备国标编码、在线状态、注册时间等字段。第一次启动如果没导入初始SQL登录Web时会出现“用户不存在或密码错误”——因为admin账号就在SQL里。4.3 编写Docker Compose编排文件我用一个精简但完整的Compose来演示生产环境可以在这个基础上扩展version: 3.7 services: mysql: image: mysql:8.0 container_name: wvp-mysql restart: always environment: TZ: Asia/Shanghai MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: wvp MYSQL_USER: wvp MYSQL_PASSWORD: wvp123 command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci volumes: - ./data/mysql:/var/lib/mysql redis: image: redis:7-alpine container_name: wvp-redis restart: always command: redis-server --appendonly yes volumes: - ./data/redis:/data wvp: image: wvp镜像:版本号 container_name: wvp-pro restart: always network_mode: host volumes: - ./config/application.yml:/opt/wvp/application.yml depends_on: - mysql - redis zlm: image: zlm镜像:版本号 container_name: zlmediakit restart: always network_mode: host volumes: - ./config/config.ini:/opt/media/conf/config.ini depends_on: - wvp在这里我故意用depends_on而不用healthcheck因为WVP启动时会自动尝试连接数据库如果MySQL还没完全初始化完成WVP会报错退出这时Compose的restart: always会拉起重试。实际部署中我更推荐加上健康检查确保顺序严谨。但要注意别把依赖写得太死——比如ZLM先于WVP启动没问题反而更稳妥因为它不依赖WVP的注册信息。4.4 启动容器并验证基础状态确认配置无误后执行docker compose up -d启动完成后看下容器状态docker ps docker logs -f wvp-pro正常启动的日志末尾能看到“启动成功”之类的日志同时Spring Boot的启动横幅会出现。如果在日志里看到数据库连接超时多半是MySQL还没Ready或者密码错了这时候先去容器内验证MySQL连接docker exec -it wvp-mysql mysql -uwvp -pwvp123 -e select 1看到返回1再重启WVP容器。这时候打开浏览器访问http://服务器IP:8080能出来登录页面说明WVP进程和前端文件都正常。默认账号密码一般是admin/admin登录后强制建议立刻改掉密码并开启操作日志记录。4.5 修改ZLM配置并启动ZLM的配置文件一般是config.ini重点改两处[api] secret你的自定义密钥 hookSecret你的hook密钥 [rtp] port_range30000-30500secret和hookSecret要和application.yml里的media.secret、media.hookKey完全一致。ZLM启动后可以通过它自带的API验证配置有效curl http://127.0.0.1:8080/index/api/version?secret你的自定义密钥如果返回了版本号json说明ZLM的HTTP接口正常密钥匹配。这里炸了很多人的点在于ZLM不同版本对config.ini的默认配置结构有变化直接用旧配置文件覆盖新镜像会报解析错误。稳妥做法是先把容器里的默认config.ini复制出来基于默认配置修改而不是网上随便下一个配置模板。4.6 接入第一台国标摄像头这里以常见的品牌摄像头为例进入摄像头Web管理后台的“接入平台设置”或“GB28181”配置页填写下列参数SIP服务器地址WVP所在宿主机IPSIP服务器端口5060SIP用户名摄像头的国标编码比如34020000001320000001SIP认证密码六位数字或自定义密码和摄像头国标密码一致设备国标编码摄像头自己的编码规则是20位数字视频通道编码通道国标编码通常是设备编码通道序号构成保存后在WVP平台左侧的“设备管理”里刷新能看到新设备向WVP注册的请求。正常情况下几秒内设备状态变为在线。如果一直是“离线”请优先检查摄像头配置页的SIP用户名和ID是否填反了——很多人把平台的ID填到摄像头上把摄像头的ID填到平台上两边鸡同鸭讲。设备在线后再点击“预览”WVP会发送INVITE给设备设备立即向ZLM推RTP流。此时观察ZLM日志会看到“收到RTP流”或“流注册成功”的日志同时WVP的Web界面通道状态从“未点播”变为“直播中”。预览画面如果出来恭喜这整套Docker部署就算真正闭环了。4.7 接入下级平台或上级平台除了直接接摄像头WVP也支持平台级联。场景A你有一个下级平台的摄像头资源希望通过WVP汇聚到上级平台场景B你的WVP是下级对接省级或市级平台。平台级联本质上也是SIP注册只不过对端是另一个国标平台。在WVP的“平台管理”中添加上级平台的SIP信息即可。需要注意级联时WVP既做Server端接收本级设备又做Client端向上级注册SIP信令中的From和To字段要和上级平台批复的编码严格一致。这种场景下最容易出的问题是上级平台下发的点播指令走到WVP后WVP去调用设备通道时发现通道编码不对——因为级联目录同步时通道编码映射规则没有统一。建议每次级联接入时先在小范围测试通道目录推送确认上级平台能看到目录树后再正式并发播测。5. 常见问题排查与实战避坑记录这部分是我实际部署多个环境攒下来的经验。我按“现象-原因-定位方法-解决方案”的方式整理成速查表方便你按图索骥。现象常见原因定位命令/方法解决方案设备一直显示离线防火墙阻挡SIP注册包tcpdump udp port 5060查看是否有注册报文放行UDP/TCP 5060设备在线但点播黑屏RTP端口范围未放行netstat -lunp或tcpdump udp portrange 30000-30500防火墙和云安全组放行UDP 30000-30500WVP能起但登录报DB异常MySQL时区/字符集错误docker logs wvp-pro定位SQL异常在MySQL连接URL中显式设置serverTimezone点播几秒后断流ZLM hookKey与WVP不一致抓ZLM日志看hook回调认证失败记录统一两个配置文件的密钥值Web端播放CORS报错ZLM/WVP跨域配置缺失浏览器F12看网络响应头在反向代理或ZLM配置中补充CORS头摄像头注册过来状态瞬时在线马上离线SIP注册周期过短或密码错误WVP日志查看认证响应码检查摄像头密码和WVP里配置的密码是否一致容器启动后WVP容器反复重启端口被宿主机其他进程占用lsof -i:5060查看占用换端口或kill冲突进程视频流延迟越来越大HLS切片机制网络丢包curl测ZLM拉流耗时调整ZLM的HLS切片时长对实时性要求高改用WebRTC除了这张表再分享几个非常容易被忽略但影响极大的细节。**第一个坑云服务器公网IP和内网IP的取舍。**如果WVP部署在云主机上sip.ip和media.ip建议填内网IP然后在云安全组放行对应端口不要想着直接用公网IP——很多云环境会把公网IP映射成NAT入口SIP信令里如果填了公网IP内网设备回包可能走不通。如果确有公网接入需求把WVP和ZLM放在一个前置安全组内严格限制源地址访问。**第二个坑摄像头国标注册密码不能太长。**很多国标摄像头固件对SIP认证密码长度有限制超过8位或含特殊字符就会注册失败。建议用一个六位数字密码同时开启WVP的“弱口令检测”功能及时发现问题账号。**第三个坑Host模式下端口冲突很难从容器状态看出来。**比如宿主机上Docker自带的iptables规则绑定了8080ZLM容器起不来日志却显示启动成功但API无响应。排查时先看宿主机的端口监听情况不要只盯容器日志。**第四个坑不要忽视WebRTC播放的IP配置。**WVP的Web端默认有多种播放协议WebRTC对网络穿透要求比较高。如果部署在公网需要配置STUN/TURN否则即使HLS能播WebRTC也会黑屏。测试基线建议用HLS或HTTP-FLV先验证流链路确认没问题后再处理WebRTC的穿透配置。6. 部署后的日常运维日志、备份与升级建议部署完成只是开始持久化运行考验的是细节。日志方面WVP的日志默认输出到logs/目录建议在Docker volume中挂载独立目录并配置logback滚动切割。application.yml里可以设置logging.level.root级别排查问题时临时调到DEBUG确认后恢复INFO避免高并发下日志打爆磁盘。ZLM的日志也建议收集它的MediaServer.log能直接看到RTP收流、RTSP会话建立、媒体超时断开等底层事件定位问题比WVP日志更直观。数据备份上MySQL里积累了设备表、用户表、级联平台表、操作日志定期做全量导出很有必要docker exec wvp-mysql mysqldump -uwvp -pwvp123 wvp /backup/wvp_$(date %F).sqlRedis里主要缓存session和部分状态Redis挂了WVP会自动重建会话不必备份得太频繁但建议开启AOF持久化容器重启后缓存不至于全丢。升级方面WVP-Pro项目迭代速度不算慢。升级前先备份数据库和两个关键配置文件application.yml和config.ini然后拉新镜像。因为WVP的自动建表只处理增量字段大的版本升级可能会执行额外的迁移脚本官方文档或升级公告里会放出来记得先执行再重启。我这里经历过的某一次跨版本升级WVP与ZLM的密钥格式变了旧的secret是纯字符串新版本要求带固定前缀启动后一直报401。后来对比了新旧镜像的默认配置才找到规律。所以升级完成后第一件事是curl ZLM的API验证密钥第二件事是尝试接入一台设备验证SIP注册这两步都过了再切业务流量。7. 生产环境部署的几个增强建议如果你不是做技术验证而是要把这套系统放到真实项目里扛流量以下几个点值得提前做。**单向TLS或HTTPS代理放前面。**WVP的Web界面如果直接暴露在公网会被安全扫描盯上。建议用Nginx或Caddy把WVP的8080包一层WebSocket升级和CORS都可以在反向代理里统一处理同时加上访问控制。ZLM的HTTP API端口不要直接暴露公网WVP和ZLM之间走内网调用就够了。**多网卡场景要指定correct IP。**有的服务器有两块网卡一块接入摄像头网段一块接入业务网段。这种情况application.yml里的sip.ip要填设备能路由到的那块网卡IPmedia.ip同理。没有统一规划的后果是设备通过网卡A注册成功但点播时INVITE里携带B网卡的IP作为接收地址设备推流就推不过来了。**摄像头数量大的时候要关注ZLM的并发能力。**ZLM是单进程多线程模型一台8核16G的服务器扛几十路并发预览没问题但如果涉及到数千路的录像回放或持续推流ZLM的RTP代理线程数和UDP缓冲区就要做调优了。部署时给ZLM容器设置ulimits比如打开文件数调高到65535防止高并发下文件描述符耗尽。还可以在ZLM的config.ini里调大maxRtpPacketSize和Socket的sendBuffer/recvBuffer但这两个参数不要盲目拉大要根据实际网络包大小来定。**视频存储规划要提前做。**WVP本身不做录像存储但它会通过ZLM的hook事件通知你自己实现录像入库、回放关联。如果项目要求录像留存建议把挂载盘做成独立卷ZLM录制目录和WVP的数据目录分开避免I/O竞争。另外国标录像回放需要SIP的PLAY方法支持WVP前端默认带的基础通道录像能力是简化版稍有规模的项目都会定制化改造这一点接入前就要跟业务方讲清楚。8. 一个小技巧把Docker部署经验沉淀成脚本最后分享一个我在多次部署后期养成的习惯把部署过程写成可重复执行的脚本而不是每次都手动敲命令。我为WVP部署写了一个初始化脚本流程拉起MySQL和Redis - 等待就绪 - 导入数据库脚本 - 校验配置文件密钥一致性 - 启动ZLM - curl校验API - 启动WVP - 等待端口监听 - 自动发送一条测试SIP消息验证注册端口。整套流程跑完大约两分钟如果哪一步失败脚本会输出当前容器日志的最后50行。这样做的好处不只是省时间。Docker部署最大的价值之一就是“环境即代码”如果你把业务侧的启动完全依赖手工操作那么环境换一台机器就又要重新踩一遍坑。把配置模板、SQL脚本、启动编排参数都收进Git仓库新环境clone下来一条命令起堆这才是用Docker的正确打开方式。我个人的体会是WVP-Pro这套系统用Docker部署真正难的从来不是启动容器而是理解SIP信令、RTP推流、HTTP API对接这三条链路在容器环境里怎么流转。把链路理清楚、把端口规划做明白、把密钥配置对齐整体部署一次跑通的概率就会高很多。如果你正在做类似项目不妨先按这篇文章的流程搭一套最小可用环境接一台设备验证全链路再逐步放大规模。