做直播监控或者给老旧设备接流的时候很多人会遇到一个很尴尬的处境摄像头只支持RTSP协议可播放端、平台方要的都是RTMP。于是windows下搭建基于nginx的rtmp服务器实现rtsp推流到rtmp就成了一个非常高频的需求。这篇博客就围绕这条链路展开记录我从零开始搭建基于nginx的rtmp服务端、用ffmpeg把RTSP摄像头流拉下来转推成RTMP流最终用VLC和ffplay完成播放验证的完整过程也会把Windows环境下的那些坑一个一个说清楚适合刚接触流媒体、想把本地摄像头接到直播链路里的朋友参考。1. 为什么需要这样一条链路RTSP摄像头推到RTMP服务器1.1 老摄像头的RTSP困局手头有一批用了好几年的网络摄像头硬件本身没有任何问题画面清晰度也够用。但这些设备当年出厂时固件只写了RTSP协议的支持没有RTMP推流能力更没有现在流行的GB28181或者ONVIF云台集成功能。想用它们做一档定期直播或者把画面转发给异地观看直接用播放器输入RTSP地址确实能看但只能一个人看没法做到多人同时稳定访问也不方便对接平台。这种场景在项目里非常常见。摄像头的RTSP地址本质上是一个拉流接口谁想看谁来拉。但拉流这种模式有几个天生的毛病第一个是并发能力有限设备端通常只允许几路会话超过就拒绝或者掉线第二个是没有统一出口无法做鉴权、录制、二次分发第三个是播放器兼容性差很多Web播放器和手机App根本没法直接播RTSP。这时候就需要一个流媒体服务器在中间做翻译官把RTSP的实时画面转成RTMP流再分发出去。1.2 为什么不在播放器里直接播RTSP有人可能会问既然VLC这类播放器能直接打开RTSP地址为什么还要费劲搭一个nginx服务器这里有一个关键区别VLC播RTSP是点对点的拉流相当于你自己拿着一根吸管去水桶里喝水吸管就一根你占住了别人就没了。而RTMP服务器的定位是水塔——先把水从水源抽到水塔里存储并转发再通过水塔给成千上万根水龙头供水。具体到实际应用我要做的是把一路摄像头画面推给直播平台或者推给多个内部观看端。如果直接拿RTSP地址去对接平台平台侧不一定支持这种协议就算支持平台每隔几秒拉取一次画面摄像头可能直接被拉崩。所以标准做法就是在本地起一个流媒体服务节点摄像头只管把RTSP流交给ffmpegffmpeg转推成RTMP流给nginxnginx再统一对外分发这样摄像头侧压力小观看侧能力也上限高很多。1.3 选型理由nginxffmpeg组合为什么够用用来做流媒体服务的方案有不少比如专门的流媒体服务器、云厂商的直播服务等。但为什么这篇选了Windows下的nginx加rtmp模块原因很简单一是轻量nginx本身性能好、内存占用低在Windows上跑起来几乎没有资源压力二是灵活通过配置一个rtmp块就能完整承担接收推流和对外分发的职责三是成本低不需要额外授权。配合ffmpeg做协议转换整个链路完全可以在个人电脑上验证跑通之后再平滑迁到服务器环境。当然nginx并不是万能的它在这个链路里只负责接收RTMP和分发RTMP不负责把RTSP变成RTMP。这个转换工作必须由ffmpeg完成。ffmpeg是目前最主流的开源音视频处理工具它支持几乎所有的音视频格式和协议用一条命令就能完成拉流和推流。接下来先把这个链路的核心原理讲清楚再动手搭建。2. 链路拆解RTSP拉流、转封装、RTMP分发到底做了什么2.1 三种协议各司其职整条链路涉及三个协议RTSP、RTMP以及底层承载的TCP/UDP。它们的分工并不相同。RTSP是控制协议它负责协商媒体会话的建立、播放、暂停等动作真正的音视频数据通常通过RTP包传输。摄像头厂商之所以都选RTSP作为对外标准接口是因为它适合做实时监控控制指令和数据传输分离延迟低。RTMP不一样它是专门为流式分发设计的协议。数据从推流端持续不断送到服务器服务器再持续分发给播放端结构上更适合直播场景。RTMP基于TCP有比较完善的连接管理、时间戳控制这决定了它在网络抖动时不容易断流播放端缓冲也能做得更平滑。这里有一个容易被忽略的点整个转换过程中音视频数据本身没有被重新编码。RTSP里的H.264视频流和AAC音频流到了RTMP里依然是同一种编码格式变的只是包装方式。所以这条链路本质上是转封装不是转码。2.2 FFmpeg真正做的事是转封装不是转码很多初学者会把转封装和转码混为一谈。一句话解释转码是改变数据的编码格式比如把H.264解码成YUV再重新编码成H.265这个过程消耗大量的CPU而转封装只是把已经压缩好的H.264数据从一个容器(比如RTSP的RTP包)里取出来再塞进另一个容器(RTMP的flv标签)里整个过程不触碰编码数据本身。ffmpeg命令行里那个-c copy参数就是这个意思。它告诉ffmpeg所有音视频流直接复制不要动编码。实际推流时CPU占用可以控制在非常低的水平。反过来如果不写-c copyffmpeg会默认对输入流做解码再编码CPU占用立刻飙升。这也是我建议链路中用ffmpeg做转推而不是用一些笨重转码程序的原因画面延迟低资源占用小一台老电脑也能带得动多路摄像头。2.3 全链路数据流向画一下这条链路的实际数据流转顺序摄像头监听RTSP端口 → ffmpeg作为客户端主动去拉取RTSP流 → ffmpeg解析出视频/音频数据封装成flv格式 → 通过RTMP协议把flv数据推送到本机nginx的1935端口 → nginx的rtmp模块接收并生成可供外部访问的RTMP流地址 → 播放端用VLC/ffplay/网页播放器访问该RTMP地址。需要注意一点在这个方案里ffmpeg是主动发起方摄像头是被动响应方。播放端不再直接访问摄像头而是访问nginx。这样架构上非常清晰摄像头只面对一个源(ffmpeg)nginx只面对一个源(ffmpeg推流)而观看端面对的则是nginx三者解耦。nginx节点即使重启ffmpeg也会因为推流失败不断重试而不至于把摄像头拉崩因为ffmpeg重试的是nginx不是摄像头。3. Windows下搭建nginx-rtmp下载、配置、启动3.1 准备带rtmp模块的nginxnginx官方在Windows上提供的安装包默认不包含rtmp模块所以第一步就是找一个已经编译好rtmp模块的Windows版本。常见做法是下载第三方编译版你也可以自己用MSYS2/MinGW编译但那很费时间而且容易踩坑普通使用不必折腾。我选择的版本是带rtmp_module的nginx稳定版压缩包。解压到纯英文路径比如D:\nginx-rtmp这一点非常重要。Windows版nginx对中文路径和带空格路径的处理一直不太友好后面配置和重启时容易遇到各种莫名其妙的问题直接放在英文目录下能省掉很多麻烦。解压之后目录结构大概是这样的conf存配置文件html存静态页面logs存日志nginx.exe是主程序。在动手修改配置之前先双击运行一次nginx.exe然后用浏览器访问http://127.0.0.1能看到nginx欢迎页就说明基础程序没有问题。这一步是为了排除程序本身跑不起来的情况让后面的排错聚焦在rtmp配置上。3.2 rtmp块配置详解用文本编辑器打开conf\nginx.conf在http块外面和events块平级的位置添加一个rtmp块rtmp { server { listen 1935; chunk_size 4096; application live { live on; record off; allow publish 127.0.0.1; deny publish all; } } }这里每一个配置都有讲究。listen 1935指定RTMP服务监听端口1935是RTMP的默认端口客户端不需要额外指定。chunk_size 4096是数据块大小直接关系到延迟和兼容性过小会增加CPU开销过大会导致某些播放器缓冲异常4096是实测比较稳妥的值。application live定义了一个名为live的应用。这个名称会出现在流地址里rtmp://127.0.0.1:1935/live/stream1。后面的stream1是流名称由推流端在推流时动态指定nginx会把这个名称和对应的流绑定起来。live on开启直播模式record off关闭录制功能避免磁盘被写满。allow publish 127.0.0.1和deny publish all是安全配置意思是只允许本机推流其他机器不能往这个nginx上随便推流。播放不受这个限制所有人都能看。3.3 启动与自检配置保存后在D:\nginx-rtmp目录下打开命令行依次执行nginx.exe -t nginx.exe -s reload-t用来测试配置文件语法看到successful字样就说明配置没问题。如果没有启动过nginx直接执行nginx.exe即可。如何确认rtmp模块生效最简单的办法是查看日志。rtmp模块是一个扩展模块启动时会把自己注册的模块列表打印到日志里。打开logs\error.log如果里面有nginx version和built with字样同时能看到与rtmp相关的模块信息就说明模块加载成功了。再配合netstat -an | findstr 1935查看端口监听状态看到一个LISTENING的1935端口服务器这一侧就绪。4. FFmpeg推流命令从摄像头拉流到推送nginx4.1 基础推流命令服务器已经就绪接下来就轮到ffmpeg登场。同样需要先下载Windows版ffmpeg并解压到英文路径然后把ffmpeg.exe所在目录加入系统PATH环境变量或者直接在命令行里切到该目录执行。核心推流命令如下ffmpeg -rtsp_transport tcp -i rtsp://用户名:密码192.168.1.64:554/stream1 -c copy -f flv rtmp://127.0.0.1:1935/live/stream1把其中的摄像头IP、路径和账号密码换成实际值。执行之后窗口会持续滚动音视频帧信息说明正在推流。这条命令看似简单但它的参数顺序、协议选择都有讲究下面逐一拆解。4.2 参数为什么这样写第一个关键参数是-rtsp_transport tcp而且必须放在-i前面。它指定ffmpeg用TCP协议去拉取RTSP流而不是默认的UDP。摄像头的RTSP默认走UDP传输UDP延迟低一些但在跨网段、丢包环境下容易出现花屏和卡顿而且Windows防火墙对UDP不太友好经常悄无声息地丢包。TCP虽然理论延迟略高但稳定性好得多实测下来画面完整性远超UDP。这个参数如果放错位置ffmpeg会直接报错或者忽略所以务必记住位置。第二个关键参数是-c copy上面已经解释过它表示直接复制编码数据不做转码。这让CPU占用降到极低实测推一路1080P摄像头CPU占用还不到5%。前提是摄像头输出的编码格式要兼容。绝大多数摄像头都能输出H.264视频流和AAC音频流这两个编码在RTMP链路中是通用格式用-c copy完全没问题。-f flv指定输出格式为flv这正是RTMP协议所要求的容器格式。最后面的推流地址里/live/对应nginx配置里的application名stream1是自定义流名称。播放时只有地址完全匹配才能拉到流。4.3 转码兜底方案如果摄像头不支持H.264输出的编码格式很特殊甚至无法被播放器识别-c copy就行不通了。这时需要让ffmpeg做真正的转码把视频转换成H.264、音频转换成AAC。命令变成这样ffmpeg -rtsp_transport tcp -i rtsp://用户名:密码192.168.1.64:554/stream1 -vcodec libx264 -preset veryfast -acodec aac -f flv rtmp://127.0.0.1:1935/live/stream1-vcodec libx264启用CPU转码把视频统一编码成H.264。-preset veryfast选择x264的编码预设越快CPU占用越低代价是压缩率略差但直播场景下码率本来就不高完全够用。-acodec aac把音频转成AAC。转码模式对CPU的要求比复制模式高得多一塔1080P硬转码在普通CPU上也会风扇狂转。所以在选择摄像头时尽量优先选H.264主流的设备如果已经买了非主流编码摄像头再考虑用转码方案兜底。5. 验证链路播放端实测与延迟观察5.1 用VLC播放验证推流命令在执行后如果窗口没有报错说明数据已经顺利进到nginx了。但推上去只是第一步能不能播才是关键。验证方式很简单打开VLC播放器选择打开网络串流输入rtmp://127.0.0.1:1935/live/stream1能够正常出画面、出声音就说明整条链路已经跑通。也可以用ffplay直接验证命令更简单ffplay -i rtmp://127.0.0.1:1935/live/stream1还有一个小技巧ffmpeg推流端窗口里会实时显示音视频帧的fps、size和time等字段。如果time字段在持续增长就说明推流正在进行播放端拉到的流才会是活的。而nginx的logs\error.log也会记录播放端的连接情况比如play stream1之类的日志这些信息对后面排查问题很有用。5.2 低延迟参数调整虽然RTMP协议本身已有较低的延迟但在实际观感中从摄像头画面变化到播放器显示仍然会有1到3秒的延迟。这个延迟主要来自ffmpeg的输入缓冲和播放器的播放缓冲可以通过参数进一步压低。在ffmpeg的输入参数里可以加上-fflags nobuffer -flags low_delay -probesize 64 -analyzeduration 0减少ffmpeg在启动时为了探测流信息而进行的缓冲ffmpeg -rtsp_transport tcp -fflags nobuffer -flags low_delay -probesize 64 -analyzeduration 0 -i rtsp://用户名:密码192.168.1.64:554/stream1 -c copy -f flv rtmp://127.0.0.1:1935/live/stream1播放端VLC也要配合调整在工具-偏好设置里把网络缓存从默认的1000毫秒调低到200毫秒左右。经过这样两端调优实测延迟能从三秒左右降到一秒以内对监控类场景已经非常实用。不过低延迟参数牺牲了一定的抗网络抖动能力如果网络质量不好还是保持默认缓冲更稳妥。5.3 再推一级转推到直播平台有时候本地验证通过还不够我需要把画面转推给外部的直播平台。这时有两种思路。第一种是让ffmpeg直接把流推给平台的RTMP地址不再经过本地nginx中转第二种是先推给本地nginx再由nginx配置一个push指令把流自动转推到平台。第二种方案更灵活因为本地nginx保留了分发能力内部播放端还能继续用。nginx配置里加上一行即可application live { live on; record off; push rtmp://直播平台地址/appkey/streamkey; }push会把进入live应用的流自动复制一份转发到目标地址。推流端只对接本地nginx不会因为平台地址变化而需要反复修改推流命令非常方便。注意这个push目标地址里的鉴权信息比如推流密钥需要从平台后台获取不同平台的地址格式略有差异。6. Windows实操踩坑我遇到过的8个问题6.1 配置路径与nginx -c的相对路径坑Windows下操作nginx第一个容易踩的坑就是路径问题。nginx在Windows上对命令行参数使用相对路径的支持很弱。比如在nginx的安装目录执行nginx.exe -c conf/nginx.conf看似合理但实际有时候会因为工作目录不对而加载不到配置。更稳妥的做法是用绝对路径nginx.exe -c D:\nginx-rtmp\conf\nginx.conf另外刚才提到的千万别把nginx解压到带有空格或中文的路径里比如D:\Program Files\nginx rtmp这种。虽然有办法通过引号绕过去但排查问题时会多很多干扰项没必要给自己挖坑。6.2 CMD转义与引号坑在Windows的CMD里执行ffmpeg命令最折磨人的就是引号和特殊字符问题。摄像头RTSP地址里经常含有账号密码有时密码里会带、^、!之类的特殊字符。在CMD里是命令分隔符如果不加引号命令会被拆成两段执行结果就是ffmpeg只拿到了地址的一部分然后报错。正确做法是把整个RTSP地址用双引号包起来。但双引号本身在CMD里也有讲究如果地址里已经包含双引号那就要在外层再嵌套转义非常痛苦。我的建议是优先使用PowerShell而不是CMD来执行推流命令PowerShell对特殊字符的处理要宽容得多。密码里如果有敏感字符建议直接找设备管理员把密码改成纯字母数字省掉所有转义烦恼。6.3 防火墙与端口占用Windows防火墙是这条链路里最容易被忽略的拦路虎。即使nginx已经在本机正常监听1935端口其他设备上的播放器也可能连不上因为防火墙默认阻止了外部对1935端口的访问。解决办法是在Windows高级安全防火墙里添加入站规则放行TCP 1935端口或者在推流和播放都在本机调试时临时关闭防火墙不推荐长期关测试完记得开回来。还有一个隐蔽的问题如果你之前装过其他流媒体软件或旧版nginx1935端口可能已经被占用。用netstat -an | findstr 1935查看端口状态如果发现端口被其他进程占用需要先结束旧进程或换一个端口。出现这种问题的时候nginx启动不会报错但外网访问就是不通极其费时间。6.4 时间戳与音频兼容问题用-c copy模式推流时最常遇到的播放异常是画面卡住不动只有声音正常或者声音正常但画面开始花屏。这种问题的根源通常是源流的音视频时间戳不同步或者视频帧关键帧间隔设置过大。RTSP流里视频帧的间隔由摄像头自身的编码配置决定有些设备默认的关键帧间隔(GOP)设置得很不合理导致播放器在发起播放时要等很久才能等到关键帧表现出来就是画面迟迟不出。解决办法有两个方向一是修改摄像头的编码设置把关键帧间隔(也叫I帧间隔)设置为2秒或更小二是不用-c copy改用转码模式在ffmpeg里通过-g 50 -r 25强制设定关键帧间隔。实测下来很多RTSP转RTMP的播放问题通过调整关键帧间隔都能解决。另外如果摄像头输出的音频编码是G711或G726这类监控设备常见编码RTMP/FLV根本不吃这种音频格式。播放时可能出现有画面没声音或者干脆推流失败。这种情况必须转码用-acodec aac把音频转成AAC后再推RTMP。这点在选购摄像头和对接老设备时要尤其注意。最后再分享一个我个人养成的习惯每次配置完nginx和ffmpeg都会把这两条命令和配置文件备份成一个文本文件放在工程目录里。看似很简单的一步实际排查问题时非常管用——因为Windows下踩过的坑大多在几个固定位置把能用的那一次记录下来等于给自己留了一份可靠的回滚方案。这套链条跑通之后后面加第二路、第三路摄像头就顺手多了无非是再起几个ffmpeg进程各自改一下摄像头地址和stream名称。如果你也是从RTSP设备一路摸过来的按上面步骤走一遍基本不会卡壳。