ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Web实时预览海康摄像头:RTSP转HLS的ffmpeg+nginx方案

Web实时预览海康摄像头:RTSP转HLS的ffmpeg+nginx方案 最近项目里接了一个很常见的需求在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方案RTSPffmpegnginx的链路依然是目前民间项目里最通用的做法之一。2. 环境准备Linux下的ffmpeg与nginx装法2.1 编译安装带rtmp模块的nginxnginx在这里承担两个职责一是作为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:password192.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:passIP:554/cam/realmonitor?channel1subtype0通道号从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可能用UDPUDP在局域网内丢包严重时画面会花屏、卡顿TCP更稳定。代价是TCP会占用稍多的带宽和连接资源但对内网摄像头来说完全不是问题。-c:v copy视频流不重新编码直接拷贝。海康主码流通常是H.264编码如果前端播放器支持H.264那copy是最省CPU的方案。一台普通服务器同时转8路copy码流都毫无压力。注意如果摄像头输出的是H.265编码那这里就不能copy了因为绝大多数浏览器的HLS播放器都不支持H.265Safari除外必须强制转成H.264后面会详细讲。-c:a aac音频转成AAC编码。海康部分摄像头有音频输入预览时需要同时听到声音统一转成AAC可以保证浏览器兼容性。如果摄像头没接音频或者不需要声音可以不加这个参数。-f hls指定输出格式为HLS。-hls_time 2每个TS分片的时长设置为2秒。这个值直接决定了延迟大小。切得越短播放器拉取新分片的频率越高延迟越低但频繁的切片会产生大量小文件对磁盘IO有一定压力。2秒是我实测比较平衡的取值既不产生过多文件延迟也能控制在5秒左右。-hls_list_size 6m3u8索引文件里最多保留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名为hlshls_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 langzh-CN head meta charsetUTF-8 title海康摄像头Web实时预览/title /head body video idvideo controls autoplay muted stylewidth: 100%; max-width: 800px;/video script srchttps://cdn.jsdelivr.net/npm/hls.js1.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] DescriptionRTSP to HLS Service Afternetwork.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 Restartalways RestartSec5 Userwww-data [Install] WantedBymulti-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路后浏览器并发解码的压力会明显增大可能要改用MSEMedia 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配置、前端播放器、权限目录、进程守护每一个环节都有一票否决的权利任何一个环节出问题屏幕上都是黑屏。所以在部署时不要把目标定在“调试通了就行”而是要把每一层都考虑到故障恢复和长期运行这样交付之后才会省心很多。最后再分享一个小技巧多路摄像头接入时建议每接一路就完整验证一路不要全部配置完了再统一排错否则问题叠加在一起排查难度会成倍增加。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进