最近项目里接了一个很常见的需求:在Web端实时预览海康威视摄像头的画面,要求不装插件、不搞ActiveX控件,最好手机和PC的浏览器打开就能看。相信做过监控对接的朋友都知道,海康官方的web插件方案只能在Windows+指定浏览器环境下跑,一旦换成Chrome新版本、Mac或者移动端,直接就废掉。后来我梳理出一套基于Linux + ffmpeg + nginx的方案,把摄像头的RTSP视频流实时转成HLS流,再通过nginx分发到Web端播放。整个过程从环境准备到最终调通,踩了不少坑,也积累了一些经验,这里完整记录下来,给被同样需求折腾的同学一个可以直接抄作业的参考。
这套方案的核心思路其实不复杂:摄像头输出RTSP流,ffmpeg负责拉流并转封装成HLS切片,nginx负责把这套切片文件通过HTTP协议吐给前端播放器。难点在于每个环节都有不少细节,比如RTSP地址怎么拼、ffmpeg参数怎么配才稳定、nginx怎么编译才带HLS模块、前端播放器怎么兼容不同浏览器。下面从方案选型开始,把每一步展开讲。
1. 为什么是“RTSP + ffmpeg + nginx”这条链路
1.1 三种常见Web视频流方案的取舍
做Web端实时监控,摆在我们面前的无非是HLS、RTMP、WebRTC三条主流路线。我刚开始也纠结过到底选哪个,后来把每种方案在真实项目里的表现捋了一遍,发现没有绝对的最好,只有场景下的最适合。
先说RTMP,它延迟低、生态成熟,Adobe时代留下来的协议,很多直播平台都在用。但问题也很明显:RTMP是基于TCP的私有协议,浏览器原生不支持,必须靠Flash或者额外的SDK才能播放。Flash早就被各大浏览器封禁了,现在再去搞RTMP,等于自己给自己挖坑。而且nginx做RTMP流分发虽然成熟,但前端播放这一环体验实在跟不上。
再说WebRTC,延迟能做到几百毫秒,真·实时通信,配合摄像头厂商或者GB28181网关可以实现很低的延迟预览。WebRTC的缺点是实现复杂度高,信令服务器、STUN/TURN穿透、媒体协商这些都要自己搭。如果不是对延迟有极致要求,比如核心安防、远程操控这种场景,前期工作量会非常可观。
最后落到HLS上,它基于HTTP协议,天然适合浏览器播放,CDN友好,也不需要额外装任何东西。HLS最大的短板是延迟偏高,常规配置下大约有3到10秒的延迟,但对于大多数监控场景——比如看看仓库有没有人、值班室瞄一眼车间状态——这种延迟完全在可接受范围内。而且HLS的兼容性极好,iOS的Safari原生支持,Android和PC端通过hls.js也能流畅播放。
1.2 这套架构在真实项目里的适用边界
这套“RTSP进、HLS出”的架构,最适合什么项目?我的判断标准很简单:只要不要求“秒级”画面同步,原生浏览器直接观看,并且不想给客户机器装任何插件,那这套方案就是性价比最高的选择。我自己做过一个值班室大屏看厂区监控的项目,8路摄像头实时预览,用的就是这套架构,nginx服务器是一台普通的4核8G虚拟机,整体跑下来CPU占用和内存都很稳,说明这套方案承载小规模的监控预览是没有任何问题的。
当然,如果未来需求升级成“毫秒级低延迟+双向对讲”,那这套架构就不能硬扛了,需要往WebRTC方向迁移,或者引入SRT这类现代传输协议。但作为一套能够快速交付、稳定运行的MVP方案,RTSP+ffmpeg+nginx的链路依然是目前民间项目里最通用的做法之一。
2. 环境准备:Linux下的ffmpeg与nginx装法
2.1 编译安装带rtmp模块的nginx
nginx在这里承担两个职责:一是作为HTTP服务器提供HLS切片文件的访问,二是借助nginx-rtmp-module模块对HLS切片做统一管理。要注意的是,nginx官方仓库里编译好的二进制包默认不包含rtmp模块,所以我们必须自己编译。我第一次编译时忘了加这个模块,结果配置里面写了rtmp指令直接报错,白白浪费了一下午。
编译前先安装依赖工具,在Debian/Ubuntu系环境下执行:
apt-get update apt-get install -y build-essential libpcre3-dev libssl-dev zlib1g-dev然后下载nginx源码和rtmp模块源码:
wget http://nginx.org/download/nginx-1.24.0.tar.gz tar -zxvf nginx-1.24.0.tar.gz git clone https://github.com/arut/nginx-rtmp-module.git进入nginx源码目录,configure时把rtmp模块加进去。这里有个小建议:最好把SSL模块和gzip模块都编译上,后面HLS走HTTPS或者调试的时候会用到。
cd nginx-1.24.0 ./configure --prefix=/usr/local/nginx \ --add-module=../nginx-rtmp-module \ --with-http_ssl_module \ --with-http_gzip_static_module make && make install编译安装完成后,先别急着配rtmp,用nginx -V检查一下模块是否真的编译进去了。输出里能看到--add-module=../nginx-rtmp-module就说明OK了。
2.2 ffmpeg安装:直接装还是自己编译
ffmpeg的安装方式有两种:一是用系统自带的包管理器直接安装,二是源码编译。我个人的建议是:如果你的Linux发行版自带ffmpeg且版本不低于4.0,那直接用系统包管理器装就行,省时省力;如果系统源里没有ffmpeg,或者版本太老导致某些参数不支持,再去源码编译。
以Ubuntu为例,直接安装:
apt-get install -y ffmpeg安装完检查版本:
ffmpeg -version关于是否源码编译FFmpeg,网上争议一直存在。我的看法是:对于RTSP转HLS这个场景,系统自带的ffmpeg已经能覆盖99%的需求,因为我们需要用到的协议(RTSP、HTTP、TCP)和解码器(H.264、AAC)都是默认编译进去的。源码编译反而容易在依赖环节出问题,比如缺少libx264库导致无法编码H.264,配置错了导致编译出来的二进制没法用,这些都是我实际踩过的坑。除非你有特殊需求,比如要引入硬件编解码器(QSV、NVENC、RKNPU),否则不要给自己加戏。
2.3 验证工具链是否就绪
工具装好之后,不要急着进下一步,先做一个快速的连通性和转码验证。在有摄像头的内网环境里,随便拿一个RTSP地址试试ffmpeg能不能正常拉流:
ffmpeg -rtsp_transport tcp -i "rtsp://user:password@192.168.1.64:554/Streaming/Channels/101" -t 10 -f mp4 /tmp/test.mp4这条命令干了这么几件事:指定RTSP走TCP传输,输入流是摄像头的RTSP地址,转码持续10秒,输出到本地MP4文件。如果命令能正常跑完并且生成了一个非空的MP4文件,说明ffmpeg和摄像头之间的链路是通的,接下来可以放心进入转流配置环节。如果这里就报错,先检查IP通不通、密码对不对、554端口是否被防火墙拦住,尽量在这一步把问题解决掉,不要带着未知问题进入上层配置。
3. 海康摄像头RTSP地址拆解与ffmpeg转流实践
3.1 海康RTSP地址格式与编码规则
对接海康摄像头,绕不开的第一件事就是拼RTSP地址。海康的RTSP取流地址有一个统一的规则,不同型号略有差异,但大体格式如下:
rtsp://用户名:密码@IP地址:端口/Streaming/Channels/编码通道号其中编码通道号是重点:101代表主码流的第一路,102代表子码流的第一路。如果摄像头接了多个通道,那203就是第二通道的子码流,以此类推。以一台双通道摄像头为例,想取第一个通道的主码流预览,地址就是rtsp://admin:你的密码@192.168.1.64:554/Streaming/Channels/101。
这里有个很实际的问题:密码里如果带了特殊字符,比如@、#、&、:这些,直接拼在URL里是解析不了的。海康默认初始密码经常是Hik12345+,里面那个+其实还好,但如果客户改成了类似admin#2024这种带井号的密码,ffmpeg拉流就会失败,报401或者403。解决办法很简单,对特殊字符做URL编码。比如#要换成%23,@要换成%40,:要换成%3A。我习惯写一个小函数处理密码,避免每次手动转义出错。
大华摄像头的RTSP地址格式也和海康类似,通常是rtsp://user:pass@IP:554/cam/realmonitor?channel=1&subtype=0,通道号从1开始,subtype 0是主码流,1是子码流。虽然本文主要聊海康,但这套思路换到大华或者其他ONVIF兼容设备上也是一样的。
3.2 ffmpeg转HLS的完整命令与参数解释
转流是整个系统的核心环节,ffmpeg把RTSP流拉下来,切成一个个小的TS切片文件,再生成一个m3u8索引文件,nginx把这些文件通过HTTP目录提供给前端。我最终使用的命令是这样的:
ffmpeg -rtsp_transport tcp -i "rtsp://admin:你的密码@192.168.1.64:554/Streaming/Channels/101" \ -c:v copy \ -c:a aac \ -f hls \ -hls_time 2 \ -hls_list_size 6 \ -hls_flags delete_segments \ -hls_segment_filename "/usr/local/nginx/html/hls/camera1_%03d.ts" \ /usr/local/nginx/html/hls/camera1.m3u8下面对参数逐个拆解,理解了这些参数,后面调优才不会两眼一抹黑。
-rtsp_transport tcp:强制RTSP传输走TCP。默认情况下ffmpeg可能用UDP,UDP在局域网内丢包严重时画面会花屏、卡顿,TCP更稳定。代价是TCP会占用稍多的带宽和连接资源,但对内网摄像头来说完全不是问题。
-c:v copy:视频流不重新编码,直接拷贝。海康主码流通常是H.264编码,如果前端播放器支持H.264,那copy是最省CPU的方案。一台普通服务器同时转8路copy码流都毫无压力。注意如果摄像头输出的是H.265编码,那这里就不能copy了,因为绝大多数浏览器的HLS播放器都不支持H.265(Safari除外),必须强制转成H.264,后面会详细讲。
-c:a aac:音频转成AAC编码。海康部分摄像头有音频输入,预览时需要同时听到声音,统一转成AAC可以保证浏览器兼容性。如果摄像头没接音频或者不需要声音,可以不加这个参数。
-f hls:指定输出格式为HLS。
-hls_time 2:每个TS分片的时长设置为2秒。这个值直接决定了延迟大小。切得越短,播放器拉取新分片的频率越高,延迟越低,但频繁的切片会产生大量小文件,对磁盘IO有一定压力。2秒是我实测比较平衡的取值,既不产生过多文件,延迟也能控制在5秒左右。
-hls_list_size 6:m3u8索引文件里最多保留6个分片的索引。配合-hls_flags delete_segments,老的分片会被自动删除,这样磁盘上始终只有大约12秒的切片文件,不会无限增长把磁盘塞满。
-hls_segment_filename:分片文件命名模板。我用camera1_%03d.ts,意思是camera1_001.ts、camera1_002.ts这样递增命名。多路摄像头时,每路都要有独立的文件名前缀,避免互相覆盖。
这个命令跑起来之后,去/usr/local/nginx/html/hls/目录下能看到不断刷新的.ts文件和camera1.m3u8文件。用tail -f camera1.m3u8可以看到索引内容在实时更新,这就说明转流已经正常工作了。
3.3 码流选型:主码流与子码流的取舍
在对接Web预览时,码流的选择是个容易被忽视但影响很大的细节。海康摄像头一般提供两路码流:主码流分辨率高、码率高,负责录像存储和需看清细节的场景;子码流分辨率低(通常是704x576或640x480),码率低,负责多路预览这种对清晰度要求不高的场景。
我在项目里一般这样取舍:如果是单路全屏预览且需要看清人脸、车牌,就走主码流;如果是大屏上同时铺4路、8路子画面,全部走子码流,清晰度够用,关键是服务器压力小。子码流的地址就是rtsp://用户:密码@IP:554/Streaming/Channels/102。
另外很多海康摄像头的编码可以在后台web管理界面调整,默认主码流可能是H.265,子码流是H.264。我的建议是:如果Web预览是刚需,并且前端没有H.265播放方案,那直接把主码流的编码改成H.264+,码率控制在4Mbps以内。这样企业场景下既保证清晰度,ffmpeg也几乎不需要转码。
4. nginx配置HLS分发与Web端播放页面
4.1 nginx-rtmp模块的hls配置
当ffmpeg把TS切片切完,m3u8索引文件生成好之后,下一步就是让nginx把这片目录通过HTTP协议暴露出去。这里需要注意的是,HLS分发的http配置不属于传统的http {}配置块,而是要走nginx-rtmp-module提供的rtmp {}配置块。这不是冲突,而是模块设计使然:rtmp模块既能做RTMP推流/拉流,也能管理HLS的切片生命周期。
我在nginx.conf里是这样配置的:
rtmp { server { listen 1935; chunk_size 4096; application hls { live on; hls on; hls_path /usr/local/nginx/html/hls; hls_fragment 2s; hls_playlist_length 12s; } } } http { include mime.types; default_type application/octet-stream; sendfile on; server { listen 8080; server_name _; location /hls/ { types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } alias /usr/local/nginx/html/hls/; add_header Cache-Control no-cache; add_header Access-Control-Allow-Origin *; } } }这个配置做了几件事:rtmp服务监听1935端口,application名为hls,hls_path指向ffmpeg写切片的目录;HTTP服务监听8080端口,/hls/路径下的URL映射到切片目录,设置了正确的MIME类型和禁止缓存,同时开启CORS跨域访问,方便前端在不同域名下播放。
配置完成后重载nginx:
/usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx -s reload然后用浏览器直接访问http://服务器IP:8080/hls/camera1.m3u8,如果页面显示一行行TS文件名(或下载了m3u8文件),说明nginx的HLS分发已经通了。
4.2 HLS缓存目录与权限处理
HLS切片文件如果没处理好权限和磁盘占用,迟早要出问题。我遇到过一种情况:ffmpeg用root用户启动,nginx用普通用户启动,导致nginx没有权限读取ffmpeg生成的TS文件,页面里播放器一直转圈,控制台报404。排查了半天,最后发现是文件权限的锅。
解决办法有两种:一是把ffmpeg和nginx都统一成同一个用户运行,二是把切片目录的权限放开。我用的就是第二种,把目录属主改成nginx用户,再给目录加写权限:
chown -R www-data:www-data /usr/local/nginx/html/hls/ chmod -R 755 /usr/local/nginx/html/hls/另外磁盘占用问题也要提前预防。前面ffmpeg的命令里加了-hls_list_size 6和-hls_flags delete_segments,正常情况下老分片会被自动清除,磁盘上只会保留最近十几秒的文件。但如果ffmpeg进程异常挂掉,残留的TS文件不会被清理,时间长了会积累很多垃圾。我写了一个简单的cron任务,每天凌晨清理一次超过半小时的TS文件:
0 3 * * * find /usr/local/nginx/html/hls/ -name "*.ts" -mmin +30 -delete4.3 Web端播放页:hls.js接入
后端链路通了,前端播放器就是最后一公里。HLS在iOS Safari里原生支持,直接在video标签里写m3u8的地址就能播,但Android Chrome和Windows上的Chrome、Firefox都不原生支持HLS,必须借助hls.js这样的JavaScript库来实现。
一套最小可用的播放页面是这样的:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>海康摄像头Web实时预览</title> </head> <body> <video id="video" controls autoplay muted style="width: 100%; max-width: 800px;"></video> <script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7/dist/hls.min.js"></script> <script> const video = document.getElementById('video'); const url = 'http://服务器IP:8080/hls/camera1.m3u8'; if (Hls.isSupported()) { const hls = new Hls({ liveDurationInfinity: true, liveSyncDurationCount: 3 }); hls.loadSource(url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { video.play(); }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // 兼容iOS Safari原生HLS video.src = url; video.addEventListener('loadedmetadata', function () { video.play(); }); } </script> </body> </html>浏览器打开这个页面,正常情况下两三秒内就能看到画面。这里有两个小细节:一是 video 标签里的muted属性,Chrome的自动播放策略要求必须有音轨且静音,否则页面不允许自动播放,如果不静音就要用户手动点击播放按钮;二是 hls.js 的liveSyncDurationCount参数,它控制播放器距离直播边缘的缓冲深度,设置为3表示播放器尽量保持与最新分片相差3个分片的距离,对降低延迟有帮助。
5. 实战中躲不开的坑:问题排查与调优
5.1 常见问题速查表
整个方案从部署到稳定运行,我前前后后遇到不少问题,这里整理成一张速查表,按出现频率从高到低排列,方便你直接对照排查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| ffmpeg拉流报401/403 | 密码错误或含特殊字符 | 检查用户名密码,特殊字符做URL编码 |
| ffmpeg拉流卡住不动 | 摄像头连接数满或网络不通 | 海康默认最多6路实时预览,断开其他预览客户端;ping和telnet确认网络 |
| m3u8能访问但播放器黑屏 | H.265编码或TS分片权限不对 | 摄像头后台把编码改成H.264;检查nignx用户能否读文件 |
| 画面有延迟且越来越大 | HLS分片列表太长或播放器缓冲设置不佳 | 调小-hls_time,设置liveSyncDurationCount |
| 多路切换时前几秒加载慢 | 播放器对长m3u8解析慢 | 提前在m3u8中只保留最近几个分片索引 |
| nginx启动报“unknow directive rtmp” | 编译时没加rtmp模块 | 重新编译nginx,确保--add-module指向rtmp模块 |
| ffmpeg进程跑几天后消失 | 内存不足或被OOM Killer杀掉 | 检查系统日志,用systemd守护ffmpeg进程 |
5.2 降低延迟的有效调整
HLS的延迟结构分为两段:分片生成延迟和播放器缓冲延迟。前者由-hls_time分片时长决定,后者由m3u8里的分片数量和播放器的缓冲策略决定。如果你的监控项目对实时性比较敏感,可以尝试按下面组合调整。
一是把-hls_time从2秒降到1秒。分片越短,播放器能越快拿到最新数据,延迟能压到3秒左右。代价是小文件数量翻倍,对磁盘IO有一定压力,但在SSD上完全扛得住。
二是调整播放器的liveSyncDurationCount,这个参数告诉hls.js还想保持多少分片的缓冲,默认值是3,可以减到2甚至1,但要留意播放器是否出现频繁卡顿,毕竟缓冲太少遇到网络抖动容易断流。
三是排查网络和转码瓶颈。如果摄像头输出的是H.265,电脑端浏览器需要转码播放,直接增加CPU压力,导致卡顿。这时候在ffmpeg里强行转换编码,用-c:v libx264 -preset ultrafast -crf 28转成H.264,虽然牺牲了一些码率和清晰度,但能保证浏览器端画质流畅。需要计算转码支撑能力时,可以按“每路720P转码大约消耗单核CPU的50%”这个经验值估算,比如4路转码至少需要2核性能余量。
5.3 保活与进程守护
部署完这套系统后,最怕的就是ffmpeg进程在半夜挂掉,第二天发现监控画面全黑,只能手动重启。用nohup启动ffmpeg虽然省事,但进程崩了没人管。我后来改用systemd服务来托管ffmpeg进程,设定了自动重启策略,稳定多了。
写一个systemd service文件,比如/etc/systemd/system/rtsp2hls.service:
[Unit] Description=RTSP to HLS Service After=network.target [Service] ExecStart=/usr/bin/ffmpeg -rtsp_transport tcp -i "rtsp://admin:密码@192.168.1.64:554/Streaming/Channels/101" -c:v copy -c:a aac -f hls -hls_time 2 -hls_list_size 6 -hls_flags delete_segments -hls_segment_filename "/usr/local/nginx/html/hls/camera1_%03d.ts" /usr/local/nginx/html/hls/camera1.m3u8 Restart=always RestartSec=5 User=www-data [Install] WantedBy=multi-user.target然后启动服务并设置开机自启:
systemctl daemon-reload systemctl enable rtsp2hls.service systemctl start rtsp2hls.service一旦ffmpeg崩溃,systemd会在5秒后自动重新拉起服务,摄像头画面就能自动恢复,不需要人工干预。多路摄像头就是多复制几份service文件,修改ExecStart里的地址和文件名即可。
这里再补充一个我实际调优的小细节:如果摄像头是通过公网或者跨网段访问,RTSP流的延迟和丢包率可能会明显升高。建议优先用TCP传输,并且可以尝试在ffmpeg命令中加入-buffer_size 1024k和-max_delay 500000参数,有时能缓解弱网下的卡顿。
6. 从“能看”到“好用”的一点扩展经验
上面把核心链路完整搭建起来之后,项目基本就能交付了。但以我的经验,真实项目的需求往往不会停留在“能看画面”这一步,而是在这个基础上追加各种要求。这里分享几个我实际做过的扩展方向。
一是多路画面同时预览。接八路摄像头时,每个摄像头启动一个ffmpeg进程,每个进程独立输出一组HLS切片。前端页面里渲染多个video标签,每个视频源指向不同的cameraN.m3u8地址。多路同时预览时,建议子码流方案,并合理控制前端同时解码的播放器数量。超过8路后,浏览器并发解码的压力会明显增大,可能要改用MSE(Media Source Extensions)统一管道来管理。
二是录像回放和存储。HLS切片本身就是在持续产生视频文件,如果希望保留录像,可以在ffmpeg转流的同时另开一路输出到MP4或者直接落盘,或者用分段存储的方式定期归档。不过录像回放一般建议直接用NVR或海康的录像方案,不要把Web预览服务器和录像存储服务器混在一起,否则磁盘和带宽压力都很大。
三是权限控制。目前/hls/目录是直接暴露的,任何知道m3u8地址的人都能访问到视频流。在正式项目里,我建议用nginx的auth_request模块配合后端的token验证来做访问控制,或者在URL中增加签名参数。视频流地址需要保护好,否则一旦泄露,别人就能随时盯着镜头看,这既是安全隐患,也会给项目带来责任风险。
四是GT28181设备的接入思路。如果项目里不是直接对接海康的RTSP,而是通过GB28181国标的设备,那流程会变成“设备→GB28181网关→RTSP/RTMP→ffmpeg转HLS→nginx分发”,核心链路不变,只是前面多了一层协议转换。大华等其他厂商的设备也是同理,先把视频流统一成RTSP,后面的链路就能完全复用。
我个人在实际操作中的体会是:这套方案真正的难点并不在“写命令”本身,而在于把它变成一个可持续稳定运行的工程。ffmpeg参数、nginx配置、前端播放器、权限目录、进程守护,每一个环节都有一票否决的权利,任何一个环节出问题,屏幕上都是黑屏。所以在部署时不要把目标定在“调试通了就行”,而是要把每一层都考虑到故障恢复和长期运行,这样交付之后才会省心很多。最后再分享一个小技巧:多路摄像头接入时,建议每接一路就完整验证一路,不要全部配置完了再统一排错,否则问题叠加在一起,排查难度会成倍增加。