ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

国标视频监控Docker Compose部署WVP-PRO+ZLMediaKit

国标视频监控Docker Compose部署WVP-PRO+ZLMediaKit 先说说我为什么写这篇东西。前阵子给一个做中小型园区安防的朋友搭监控平台设备是几十路国标GB28181的摄像头需要统一接入、实时预览、录像回放。我一开始在他那台Ubuntu 22.04服务器上手动装WVP-PRO和ZLMediaKit装到一半发现依赖版本互相打架MySQL、Redis、JDK、Maven全都得伺候折腾了两个晚上才把服务跑起来结果朋友一个docker compose ps都看不明白后续维护更是灾难。后来我推倒重来老老实实改用Docker Compose一套编排把WVP-PRO、ZLM、录像服务、Nginx反代全部容器化整个部署过程压缩到一小时内。这篇教程就是那次完整重建过程的记录属于那种“我要是早点看到就能少掉两根头发”的保姆级内容。你如果手里正好有国标摄像头要接入、要录像、要回放这个方案可以整套抄走。文章比较长但每一步都是实测过的跟着做基本不会卡壳。1. 先搞清楚WVP-PRO和ZLM到底在系统里各自干什么很多人一上来就急着复制docker-compose.yml结果跑起来发现摄像头上线了但不出流或者录像目录空的然后开始怀疑人生。要避免这种局面第一步不是敲命令而是搞清楚这堆容器里每个角色的职责边界。1.1 国标接入的那套信令与媒体分离逻辑GB28181是国标视频监控的通信协议它的核心设计是信令和媒体分离。翻译成白话就是摄像头和控制平台之间跑着两套完全不同的数据流。信令通道走的是SIP协议Session Initiation Protocol会话初始协议负责设备的注册、心跳、邀请、查询这些控制操作。摄像头开机后会把自己注册到某个SIP服务器上告诉服务器“我在线、我在这、我支持什么编码”。这层通信的特点是数据量小、频率低但对可靠性要求高。媒体通道走的才是真正的视频流摄像头被平台“邀请”之后通过RTP/RTCP协议把H.264或H.265的视频流推出来。这层数据量大、实时性要求高对网络端口的要求也很苛刻。在这个架构里WVP-PROWVP视频平台就是那个SIP信令服务器它负责跟摄像头喊话、做设备管理、用户认证、录像计划编排以及把信令和媒体层粘合起来。ZLMediaKit简称ZLM就是那个媒体流转发引擎负责接收摄像头推上来的RTP流转封装成RTMP、RTSP、HLS、WebRTC等不同协议再分发给播放端。同时它还有一个很实诚的录像功能——直接按计划把流录制为MP4文件。你从Web界面看到的设备列表、通道列表、播放画面、录像回放上层是WVP在做业务控制底层全部落到ZLM在搬砖。1.2 这套组合的典型架构和容器分工用Docker Compose部署的时候实际上一共跑这么几个容器容器服务镜像/角色核心职责mysqlMySQL 8.x存储WVP的设备信息、用户信息、录像计划、操作日志redisRedis 7.x缓存会话、临时状态、异步任务队列wvp-proWVP-PRO服务SIP信令处理、RESTful API、Web管理界面zlmZLMediaKit流媒体接收、转封装、分发、MP4录像落盘nginxNginx反向代理Web界面和流媒体接口、SSL终结、统一入口这里要特别强调WVP和ZLM是强耦合的。WVP在信令层面把摄像头“邀请”上来之后会在内部给ZLM派活——告诉它去哪个端口收流、收完流怎么转协议。ZLM收到流之后又会通过HTTP hook回调告诉WVP“流已经上线了”。如果这两个服务之间通信不畅整条链路就断了。1.3 为什么非要用容器编排而不是裸机部署我自己在裸机上踩过大坑对照下来容器编排有几个压倒性优势第一依赖隔离。WVP-PRO需要JDK 8以上ZLM需要特定版本的编译环境和FFmpeg依赖库MySQL和Redis又有自己的版本要求。裸机装的时候一个库的版本冲突能让整个系统罢工。容器把每份依赖锁在自己的镜像里互不干扰。第二环境可复现。同一份docker-compose.yml在开发机、测试机、生产机上跑出来的结果完全一致。不会出现“我本机明明是好的部署到服务器就废了”的玄学问题。第三维护成本断崖式下降。升级镜像就是改一行版本号再docker compose pull docker compose up -d回滚也是改回来就行。裸机部署想升级一次JDK或者ZLM版本怕是要折腾半天。第四资源占用可控。这几个容器加起来内存占用大概在1GB到2GB之间CPU在空闲状态基本可以忽略。对一台4核8G的入门级服务器来说跑这套完全没有压力。2. 部署前的准备端口规划、目录划分与镜像版本选择部署之前先把规划做细后面能省一大半排错时间。我那次就是因为端口规划没做好摄像头注册上来了RTP流却怎么都收不到排查了半天发现是UDP端口的防火墙规则没放行。2.1 一张端口清单把所有通信链路理清这套系统涉及到的端口比较多我一般建议拿张小本子先把端口写清楚再开始部署。下面这张表是我实际使用的端口规划你可以直接照抄端口归属服务协议用途3306MySQLTCP数据库访问仅容器内网通信不对外6379RedisTCP缓存访问仅容器内网通信不对外18080WVP-PROTCPWeb管理界面HTTP端口18081WVP-PROTCPWeb管理界面HTTPS端口反代后可不对外开放5060WVP-PROTCP/UDPSIP信令端口必须对摄像头开放1935ZLMTCPRTMP流媒体端口554ZLMTCPRTSP流媒体端口80ZLMTCPZLM内置HTTP API端口hook回调用332ZLMTCP/UDPRTP收流端口接收国标推流10000~10010ZLMUDPRTP收流端口区间每路流占一个端口30000~30050ZLMUDPRTP收流扩展端口区间路数多时使用16200ZLMTCP用于HTTP-TS等流媒体拉流端口这里重点提醒两件事。第一SIP信令端口5060需要同时支持TCP和UDP。很多调试工具默认只开TCP结果摄像头注册总是超时。国标协议里注册和心跳这些信令消息可以走TCP也可以走UDP但实际设备实现五花八门有些老设备只支持UDP你只放行TCP就会注册不上。第二ZLM的RTP收流端口是UDP区间端口。这是一整块UDP范围而不是单个端口。原因在于ZLM每接收一路国标摄像头的推流就会在这个区间里动态分配一个UDP端口。如果端口区间开得太小并发推流超过端口数量就会随机失败。具体的区间大小跟你的摄像头路数直接相关建议至少开放10个端口起步规模大就按“路数50%余量”来规划。2.2 目录与数据卷划分数据库、配置、录像必须分开我见过有人在部署时图省事所有数据都放到容器里头结果容器一删设备配置、录像文件全部消失。这个坑必须绕开。强烈建议在宿主机上建立独立的目录结构/opt/wvp-docker/ ├── docker-compose.yml ├── mysql/ │ └── data/ # MySQL数据目录 ├── redis/ │ └── data/ # Redis持久化目录 ├── wvp/ │ └── config/ # WVP配置文件容器内挂载 ├── zlmediakit/ │ ├── config/ # ZLM配置文件容器内挂载 │ └── media/ # 录像/媒体文件存储目录 └── nginx/ ├── conf/ # Nginx配置 └── logs/ # Nginx日志我习惯把录像目录单独挂在独立的数据盘上。举个例子如果你的服务器有块专门的数据盘挂载在/data那就把目录规划成/data/zlmediakit/media不要跟系统盘放一起。录像文件的写入频率高、单文件体积大如果跟系统盘抢IO系统负载一高摄像头信令可能都会受影响。2.3 镜像版本对应关系与网络模式选择镜像这里我吃过亏版本对应关系一定要看清。WVP-PRO的官方Docker镜像和库在64854coding这个腾讯工蜂仓库里你直接docker pull可能拉不到建议先拉下来再重新打标签。具体版本对应关系如下组件推荐版本说明MySQLmysql:8.08.0以上版本对JSON类型的支持更好WVP的某些初始化脚本用到了Redisredis:7.07.x性能好且稳定兼容性也没有问题wvp-prowvp-pro:latest自定义构建官方仓库拉下来后需要打标签建议固定sha值不用latestZLMediaKitzlmediakit:latest自定义构建保持与WVP的hook API兼容Nginxnginx:1.24-alpineAlpine版本体积小跑反代足够网络模式我建议使用Compose默认的bridge网络这个是最稳的选择。不要图省事直接network_mode: host虽然省去了端口映射的麻烦但会让容器间通信变得混乱尤其在Nginx反代转发场景下指代IP稍微写错就连不上。在bridge网络里容器间可以通过服务名互相访问。比如WVP容器里配置ZLM的API地址直接写http://zlm:80而不是http://192.168.x.x:80。这个逻辑在配置hook回调的时候尤其重要后面有专门的章节讲。3. docker-compose.yml逐段拆解每一行配置对应的实际作用这段是整个部署的核心。我先把完整版的docker-compose.yml拿出来然后逐段拆解为什么这么写。version: 3.8 services: mysql: image: mysql:8.0 container_name: wvp-mysql restart: always environment: MYSQL_ROOT_PASSWORD: Wvp123456 MYSQL_DATABASE: wvp TZ: Asia/Shanghai command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_general_ci - --lower_case_table_names1 volumes: - /opt/wvp-docker/mysql/data:/var/lib/mysql - /opt/wvp-docker/mysql/init:/docker-entrypoint-initdb.d healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -pWvp123456] interval: 10s timeout: 5s retries: 10 redis: image: redis:7.0 container_name: wvp-redis restart: always command: redis-server --appendonly yes --requirepass Wvp123456 volumes: - /opt/wvp-docker/redis/data:/data healthcheck: test: [CMD, redis-cli, -a, Wvp123456, ping] interval: 10s timeout: 5s retries: 10 wvp: image: wvp-pro:latest container_name: wvp-pro restart: always depends_on: mysql: condition: service_healthy redis: condition: service_healthy environment: TZ: Asia/Shanghai volumes: - /opt/wvp-docker/wvp/config:/opt/wvp/config - /opt/wvp-docker/wvp/logs:/opt/wvp/logs ports: - 5060:5060/tcp - 5060:5060/udp - 18080:18080 - 18081:18081 zlm: image: zlmediakit:latest container_name: zlmediakit restart: always depends_on: - wvp environment: TZ: Asia/Shanghai volumes: - /opt/wvp-docker/zlmediakit/config:/opt/media/conf - /opt/wvp-docker/zlmediakit/media:/opt/media/bin/www network_mode: service:wvp depends_on: - wvp nginx: image: nginx:1.24-alpine container_name: wvp-nginx restart: always depends_on: - wvp ports: - 80:80 - 443:443 volumes: - /opt/wvp-docker/nginx/conf:/etc/nginx/conf.d - /opt/wvp-docker/nginx/logs:/var/log/nginx这份内容的挂载关系是经过验证的下面逐段拆解。3.1 MySQL与Redis为WVP准备的后端服务MySQL这一段有几个容易被忽略的细节。--lower_case_table_names1这个参数是必须加的。WVP的数据库脚本里表名既有大写又有小写而MySQL 8.0在Linux系统上默认是区分大小写的不加这个参数初始化脚本会报一堆表不存在的错。初始化SQL脚本的挂载位置在/opt/wvp-docker/mysql/init目录MySQL容器首次启动时会自动执行该目录下的.sql文件帮你建好库表结构。这个初始化动作只在数据目录为空时执行一次如果数据目录已经存在内容后续重复挂载不会重新执行。Redis这里用了--appendonly yes开启AOF持久化并设置了密码。有人可能会问WVP的内部服务通信Redis有必要加密码吗我个人建议加上。因为docker-compose.yml文件很容易被分享出去或者截图传阅如果Redis无密码且端口映射到了公网攻击者直接连上来扫缓存数据里面可能有设备的鉴权信息。加一层密码不会增加多少运维负担但安全性提升一个档次。3.2 WVP-PRO数据库初始化与环境变量WVP容器首次启动时会自动尝试连接MySQL初始化数据库。等MySQL容器是healthy状态再启动WVP这个依赖关系由depends_on加condition: service_healthy控制能保证数据库已经准备就绪。环境变量这里我只设置了TZAsia/Shanghai。WVP真正的配置项在application.yml里通过挂载目录的方式读入。有人会问为什么不用环境变量去传MySQL密码、Redis密码这些WVP的官方镜像本身支持环境变量覆盖配置但实际测试下来部分老版本的镜像对环境变量的读取逻辑比较简单粗暴直接改application.yml是最稳的方式。WVP的配置目录挂载到/opt/wvp/config首次启动时如果该目录为空容器会把默认配置模板复制一份出来。所以正确的操作顺序是先把容器跑一遍生成默认配置然后停掉容器修改配置再启动容器。不要提前往配置目录里塞空文件那会让容器无法生成默认配置。3.3 ZLMconfig.ini挂载方式与secret的口子ZLM这段我踩过最大的坑就是直接挂载宿主机空目录到/opt/media/conf结果容器起不来。因为ZLM启动时会往conf目录写入很多默认配置和配置文件你给它一个空目录它自己会生成一份看起来没问题但如果这个空目录的属主不是容器内运行用户后续写入过程就会报权限错误。我的做法是先让ZLM容器挂载一个已经存在的目录并把目录权限设为chmod -R 777容器启动时会在里面生成config.ini。生成之后停掉容器再修改config.ini里的关键项再启动。这个顺序能避开绝大多数的权限坑。ZLM的config.ini里有两个关键配置必须检查[api]下的secret这是ZLM的API鉴权密钥。WVP调用ZLM的API时需要携带这个secret。WVP配置里的media.secret必须和ZLM的config.ini里的secret一致否则会出现“流媒体服务器连接失败”的报错。[hook]下的enable和on_flow_report等回调地址ZLM发布流成功后会回调WVP的接口通知业务层“流上线了”。这个回调地址如果写错最直接的现象就是——摄像头已经在ZLM里能看到流了但WVP的Web页面上依然显示离线或看不到画面。3.4 健康检查、重启策略与依赖启动顺序这段是Compose编排里最容易被忽视、但恰恰是稳定性的关键。restart: always让容器异常退出后自动重启这个直观易懂。难点在于启动顺序。WVP依赖MySQL和Redis如果数据库还没就绪WVP就先启动了WVP会报连接超时然后直接退出。加了depends_on加condition: service_healthy之后Compose会等MySQL的healthcheck通过再启动WVP。MySQL的healthcheck我用的命令是mysqladmin ping -h localhost -p密码。这里有个小坑MySQL容器刚启动时mysqladmin ping是能成功的但此时可能还没完成初始化。保险做法是把retries调大一点比如10次每次间隔10秒给足MySQL初始化的时间。ZLM这里用了network_mode: service:wvp把ZLM和WVP放进同一个网络命名空间。这么做的好处是ZLM的hook回调地址直接可以用http://127.0.0.1:18080访问WVP不需要依赖Docker的服务名解析。这个写法的缺点是ZLM不能单独映射端口但因为我们只需要通过WVP同一个网络栈对外服务完全够用还省掉了一批端口映射的维护项。4. 打通WVP与ZLM的鉴权和钩子回调这一章是整个部署过程中最容易让人半路放弃的环节。我把每个互联网上模模糊糊的细节都摊开讲透。4.1 secret配置不一致会出现的典型症状WVP和ZLM之间通过HTTP API通信ZLM的API接口都要求携带secret参数做鉴权。WVP的application.yml配置和ZLM的config.ini里都有这个secret字段。常见的部署报错长这样2024-xx-xx 10:23:45.123 ERROR - [MediaServer] 连接流媒体服务器(ZLM)失败: 401 Unauthorized, secret error问题的本质很简单两边的密钥对不上。我在排查这个问题的时候发现很多人喜欢把WVP里的media.secret留空觉得不填最省事。可ZLM默认的secret也不是空的它有个默认值两边一对比就报了鉴权失败。正确做法是在ZLM的config.ini里设置一个强一点的secret比如your-strong-secret-2024然后在WVP的application.yml里把media.secret配置成同一个值重启两个容器让配置生效。4.2 hook回调地址在容器网络里的正确写法ZLM的hook是这样一个机制每当流上线、流关闭、录像开始、录像结束等事件发生时ZLM向配置好的HTTP地址发起POST请求把事件信息推给WVP。这个回调地址如果不对事件通知就到不了WVP。这里有两种场景地址写法完全不同场景一ZLM和WVP在同一网络命名空间就像我们这种Compose方案这时ZLM容器内访问WVP直接写http://127.0.0.1:18080/index/hook就好因为network_mode: service:wvp让两者共享了网络栈。127.0.0.1在容器里既指代WVP又指代ZLM。场景二ZLM和WVP在不同容器、使用普通bridge网络这时要用服务名来访问写成http://wvp:18080/index/hook。注意这里不能用公网IP因为容器间的流量只走Docker内部网络公网IP不一定能回环到达容器内。我有个朋友反代想外界访问流畅把hook回调地址写成了http://公网IP:18080/index/hook实际跑起来画面一直加载不出来。原因就是容器流量走Nginx再绕回来链路多了一条还容易出现跨域问题。4.3 录像相关hookon_record_mp4从ZLM到WVP的链路如果你只是想要实时预览不配置录像相关hook也能用。但录像开始、录像结束这些事件要联动到WVP的业务层就必须把on_record_mp4这个hook配置好。它的流程是WVP在数据库里创建了一条录像计划到期后通过API通知ZLM开始对某条流录制MP4。ZLM录制完成一段MP4文件后根据on_record_mp4配置的地址向WVP发一个POST请求把录像的路径、文件名、文件大小、时间戳等推给WVP。WVP收到通知后在数据库里写入一条录像索引记录。前端回放页面才能列出来这段录像。如果on_record_mp4没配置或者地址不对录像文件依然会在ZLM的磁盘上生成但WVP的web回放页面里会看不到录像或者点击回放时提示没有录像数据。在config.ini里这些hook回调地址一般长这样[hook] enable1 on_flow_reporthttp://127.0.0.1:18080/index/hook/on_flow_report on_stream_changedhttp://127.0.0.1:18080/index/hook/on_stream_changed on_playhttp://127.0.0.1:18080/index/hook/on_play on_record_mp4http://127.0.0.1:18080/index/hook/on_record_mp4每一行都对应一种事件如果你不确定当前WVP版本支持哪些hook去WVP的application.yml里找hook相关的配置块官方默认模板里列得明明白白。5. 录像服务从数据库初始化到录像文件落盘录像功能不是配置一个开关就完事的它涉及数据库表结构、ZLM录制模块、磁盘目录、回放接口四个层面的协作。5.1 WVP首次登录后的数据库初始化WVP容器启动之后浏览器访问http://服务器IP:18080默认账号admin默认密码admin289。登录进去之后你会发现页面是能打开的但很多功能模块是空的。这时候要检查MySQL的wvp数据库里是否已经创建好了所有表。如果没有那就要手动执行WVP项目里附带的初始化SQL脚本。这些脚本一般在WVP项目的sql/目录下文件名类似wvp_2024xxxx.sql。如果你使用我上面那套Compose方案在MySQL容器首次启动时它就自动从这个目录执行了初始化脚本。但如果你的/opt/wvp-docker/mysql/init目录在首次启动时是空的那一开始就没建表后面想补就得多走一步手动执行docker exec -i wvp-mysql mysql -uroot -pWvp123456 wvp wvp_20240401.sql执行完之后刷新一下WVP后台页面你就能看到“设备管理”“录像计划”这些菜单都正常了。这个初始化动作不能漏漏了后面摄像头注册上来了你看不到设备列表还以为是摄像头的问题其实是库表没建好。5.2 录像计划的配置入口和ZLM录制的对应关系WVP后台“录像计划”功能里你可以按通道设置录像计划。比如某个通道只在工作日9点到18点录像另一个通道全天候录像。这个计划的生效链路是这样的WVP把计划持久化到MySQL然后按照计划的时间点通过ZLM的API发起录制请求。ZLM收到请求后对流数据进行MP4封装并写入本地磁盘。配置录像计划时有几个细节要留意通道必须在线。如果通道离线WVP不会为离线的通道发起录制请求。等通道恢复上线后需要检查录像计划是否还在生效有些版本在通道离线再恢复后老的计划会失效。录制时间片。ZLM的录制默认是分片存储的我建议把分片大小设为60分钟或者120分钟。分片太短会产生大量小文件对磁盘inode消耗很大分片太长某一段录像损坏了这个时间段的内容就全丢了。录像存储位置。ZLM的config.ini里录像目录默认在/opt/media/bin/www下我挂载到宿主机的/opt/wvp-docker/zlmediakit/media。录像文件按record/日期/应用名/流ID/这样的层级组织。如果录像目录里出现了文件但WVP回放页面没有记录优先检查on_record_mp4hook的回调地址是否正确。5.3 录像文件的目录映射和回放验证部署完之后怎么确认录像真的在录最快的办法是直接在宿主机上看目录find /opt/wvp-docker/zlmediakit/media/record -type f | tail -20如果文件在持续生成说明ZLM录制正常。再用ffprobe验证一下文件能不能正常解码ffprobe -v error -show_format -show_streams /opt/wvp-docker/zlmediakit/media/record/2024-04-01/live/stream_id/xxx.mp4 | grep -E codec_name|duration正常输出里应该有h264或h265的编码信息以及不为零的时长。回放阶段的完整链路是用户在WVP页面点击某条通道的“回放”WVP先查MySQL里有没有该时间段的录像索引有则向ZLM请求一个回放流地址播放器拉流播放。我习惯再做一步外部播放验证用VLC直接打开ZLM的RTSP回放地址rtsp://IP:554/record/live/stream_id/2024-04-01.mp4能正常出画就说明流媒体链路通问题只可能在WVP到前端播放器的环节。6. Nginx反代部分HTTP与流媒体端口的统一入口反代这部分一开始我是拒绝的——反正服务都能通过IP加端口访问为什么还要多套一层Nginx后来发现当你要给客户交付系统、要在页面上隐藏WebSocket端口、要做HTTPS证书终结的时候没有反代配置会散落得到处都是。6.1 为什么需要反代以及Http块配置归纳下来反代至少解决四个问题端口统一。对外只暴露80和443内部WVP的18080端口、ZLM的API端口都不用暴露到公网减少被扫描攻击的面。HTTPS证书终结。让你能用https://wvp.example.com访问后台证书只需要在Nginx上配一份后端服务不用改。路径路由。同一个Nginx服务上可以把/路由到WVP把/live/路由到ZLM的流媒体服务。WebSocket代理支持。WVP前端页面与后端通信会用到WebSocketNginx需要配置对应的Upgrade头才能正确透传。下面是我实际使用的Nginx配置注意WebSocket的请求头设置server { listen 80; server_name wvp.example.com; location / { proxy_pass http://wvp:18080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_connect_timeout 60s; proxy_read_timeout 3600s; } }proxy_read_timeout 3600s这个设置很关键。播放器通过WVP拉流时WebSocket连接可能会长时间保持默认的60秒超时会让播放一会儿就断开。我一开始没加导致10分钟左右的监控画面就闪断一次排查了很久才发现是Nginx的超时在作怪。如果你需要把ZLM的流媒体播放地址也暴露出来可以加一个单独的locationlocation /live/ { proxy_pass http://zlm:80/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }注意这里proxy_pass后面的URI和location的匹配关系。http://zlm:80/结尾带斜杠会把/live/后面的路径直接拼上去这样ZLM收到的请求路径就跟它原生一致了否则会因为路径多了一层前缀导致404。6.2 流协议端口用stream模块转发HTTP协议用Nginx的http模块反代但RTSP、RTMP这类流协议不是标准HTTP用location块处理不了。这时候要开Nginx的stream模块。在nginx.conf里加上stream { upstream zlm_rtsp { server zlm:554; } upstream zlm_rtmp { server zlm:1935; } upstream zlm_rtp { server zlm:332; } server { listen 554; proxy_pass zlm_rtsp; proxy_timeout 3600s; proxy_connect_timeout 10s; } server { listen 1935; proxy_pass zlm_rtmp; proxy_timeout 3600s; proxy_connect_timeout 10s; } server { listen 332 udp; proxy_pass zlm_rtp; proxy_timeout 3600s; proxy_connect_timeout 10s; } }这里有个容易踩的坑Nginx的stream模块和http模块不能写在同一个server块里它们是不同上下文。stream块必须放在nginx.conf的顶层和http块平级而不是放在某个server里面。UDP的RTP流如果端口数量多也可以在stream块里用一个listen区间来做转发server { listen 10000-10010 udp; proxy_pass zlm_rtp; }这样就能把ZLM的RTP收流端口区间也统一包到Nginx后面外网摄像头推流地址只需要指向Nginx的IP和端口。6.3 回调地址在反代场景下最容易搞混的关系反代一上就出现了一个新的地址关系外部请求和内部回调请求走的是完全不同的路径。外部请求浏览器 - Nginx 80/443 - WVP 18080这是用户访问Web页面、拉流的路径。内部回调ZLM - WVP 18080不走Nginx这是ZLM向WVP推送事件通知的路径。判断这个链路是否出问题的经验是如果页面能打开、设备也能注册上来但录像记录不生成那多半是内部回调路径断了如果页面打不开那是外部入口的问题。在反代场景下我建议ZLM的hook回调仍然写http://127.0.0.1:18080/index/hook因为两者共享网络命名空间不要改成https://wvp.example.com/index/hook。因为Nginx的HTTPS终结之后要把请求再转发给WVP内部的HTTP端口又多一层可以出错的地方。而且如果Nginx配置的client_max_body_size比较小大体积的hook请求还可能被Nginx拒收。7. 上线后的核心技术自查清单与排障链路部署完成不是终点能稳定跑起来才是。这章是我实战中最常被人咨询的排障合集我把排查思路按链路顺序写出来遇到问题直接对照步骤走。7.1 摄像头上线但不出流的七步排查链路这是最高频的问题摄像头注册上来了设备列表里能看到在线状态但点开播放一直转圈不出画面。我固定按下面这七个步骤排查基本能定位90%以上问题步骤检查对象操作/命令期望结果1摄像头侧状态在WVP后台看通道在线状态在线且通道ID正确2WVP信令日志docker logs -f wvp-pro --tail 200看到SIP invite发送记录3ZLM收流状态docker logs -f zlmediakit --tail 200看到on_publish回调记录4ZLM端流注册访问ZLM APIhttp://IP:80/index/api/getMediaList?secretxxx有对应stream_id的流信息5ZLM到WVP的hook检查WVP日志是否有on_stream_changed记录有记录说明hook通了6播放器拉流链路用VLC直接拉rtsp://IP:554/live/xxx能出画面说明流媒体链路正常7Nginx转发链路检查Nginx错误日志外网拉rtsp://域名:554/live/xxx能出画面说明反代正常这个链路的核心逻辑是从摄像头端一路往播放端推移每一步检查都验证前一步的下游是否正常。举个例子如果第2步WVP日志里压根没有SIP invite记录那问题其实出在WVP没有收到摄像头的推流请求或者信令端口不通。这时候不需要检查ZLM因为ZLM根本没有被WVP通知去收流。如果第4步ZLM端有流但第5步WVP日志里没有on_stream_changed那就是hook回调地址配置的问题按前面第4.2节的方法改回调地址。7.2 UDP端口未开放这个最大的隐形坑我专门把一个坑单独拎出来讲因为这个坑的表现太有迷惑性了。现象是设备在线、WVP日志显示invite已发送、ZLM日志显示收到RTP流了但画面就是不显示。查了很久最后发现是云服务器安全组和宿主机防火墙只开了TCP端口UDP端口被默认丢弃。国标推流走的是UDP的RTP如果UDP端口不通ZLM收不到流显示出来的就是连接超时。但有意思的是设备在线状态是靠SIP注册心跳维持的而SIP信令刚才讲了也走5060端口。如果5060只开放了TCP且设备恰好走TCP注册那设备列表照样显示“在线”让你误以为网络全部通畅。排查时用下面这条命令直接测试UDP端口通不通nc -uvz 服务器IP 332或者用tcpdump抓包看是否有UDP包到达ZLM容器docker exec -it zlmediakit sh -c cat /proc/net/udp tcpdump -i any udp port 332 -n -c 50如果看到大量的UDP包被丢弃基本可以确定是防火墙或安全组把UDP端口挡了。放行规则要同时覆盖UDP 5060SIP信令UDP 332RTP基础收流UDP 10000-10010RTP动态端口区间7.3 我踩过的几个坑和对应的规避方案最后把我自己部署和维护过程中印象最深的四个坑分享出来每一个都对应一条实际的规避经验。坑一镜像拉不下来导致部署中断WVP-PRO和ZLMediaKit的镜像在部分网络环境下可能不太好拉取。我的应对方案是如果官方仓库比较吃力考虑走代理镜像源或者从内网镜像仓库拉取后手动打标签。另外docker pull的时候注意指定完整仓库地址不要用简写避免拉错镜像。坑二录像文件越来越大磁盘被写满录像是一头吞磁盘的空间怪兽。按一台200万像素摄像头、H.264编码、码率4Mbps来算一天的录像量大约是40GB出头。二三十路通道跑一个月就是几十TB。我的建议是录像目录一定要独立挂载到大容量数据盘写一个定时任务按保留天数清理旧录像文件提前规划好存储容量别等磁盘满了才发现坑三容器重启后服务起不来因为端口被占用常见场景是服务器开机后某些进程提前占用了端口WVP或ZLM容器无法绑定。排查方式ss -lntup | grep -E 5060|332|18080如果发现端口被其他进程占用要么停掉冲突进程要么改容器端口映射。这个坑多发生在你用同一台服务器部署过别的业务的情况下。坑四WVP升级后数据库表结构对不上WVP版本更新频繁数据库表结构可能随之变化。我建议升级前先备份数据库docker exec wvp-mysql mysqldump -uroot -pWvp123456 wvp wvp_backup_$(date %F).sql升级后再跑增量SQL脚本。不要图省事直接覆盖库表生产环境的设备数据丢了很难恢复。8. 我个人在实际操作中的一些体会整套系统跑到现在我对这套方案的评价是稳定、省心、扩展性好。但要说“一键部署真的一点坑都没有”那是骗人的。真实体验是Compose编排把基础设施的复杂度收敛了但WVP和ZLM之间那层对接逻辑以及对于流媒体协议的理解才是真正决定你能不能顺利上线的关键。如果让我给后来者三条最实在的建议我会说第一先单机单流跑通再加路数。别一上来就把几十路摄像头全接入。先用一支摄像头做完“注册-直播-录像-回放”全链路闭环验证没有问题了再慢慢把剩余通道接进来。这样即使出问题范围也小、好排查。第二配置文件的每一次修改都要有记录。我习惯在修改application.yml和config.ini的地方写注释说明改了什么、为什么改。很多次排查问题时翻注释能迅速回忆起当初的意图省去大量的反向推理时间。第三注意版本冻结。WVP、ZLM这两个组件迭代速度很快今天能用的配置下周升级后可能就不兼容了。生产环境建议固定镜像的sha256值不要用latest标签。升级时先在测试环境验证一遍再应用到生产。这套环境现在还在稳定的运行每天录着几十路的视频摄像头掉线也能自动实名上线回放检索速度也很快。视频监控这种系统稳定性比花哨更重要用Docker Compose把每个组件管好、把数据目录规划好、把备份脚本写好剩下的交给时间就好。
返回列表