内网低延迟直播方案:OBS+FFmpeg实现H264 RTP组播分发

发布时间:2026/10/2 13:16:46
内网低延迟直播方案:OBS+FFmpeg实现H264 RTP组播分发 做局域网直播分发这件事很多人第一反应是架个 RTMP 服务但真干过几次就会明白RTMP 那套在公网上能用放到内网多点分发场景里延迟高得离谱而且服务器要硬吃一遍转发的流量接收端一多就开始抖。我这几年在公司内部折腾各种直播系统年会、培训、大屏联动、产线监控都碰过试过 RTSP、试过 WebRTC、也试过各种流媒体服务器最后最顺手的一套组合就是OBS 负责采集和画面合成FFmpeg 负责编码和组播发送传输层走 H264 裸流的 RTP 组播。这套方案能把端到端延迟压到 150 毫秒以内接收端数量几乎不设上限一个组播流能同时喂整个内网的终端而且还不用部署额外的流媒体服务端。下面把整套配置从头到尾拆一遍包含 OBS 的接入方式、FFmpeg 的编码参数调优、组播网络配置以及我踩过的各种坑适合想低成本搭内网低延迟直播的运维、音视频工程师和刚入门 OBS/FFmpeg 的玩家参考。1. 整体思路与方案选型1.1 为什么最后选了 RTP 组播先说我这几年实际摸过的几条路你大概就能理解为什么最后会落在 RTP 组播上。RTMP 推流加播放是最常见的套路但它是 TCP 上的长连接一台播放器就要占一路推流或者一路转发服务器带宽压力一直都在。而且 RTMP 为了兼容性和弱网抗性普遍要走 nginx-rtmp 或者 SRS 之类的服务端做中转客户端缓冲动不动就是 1 到 5 秒。拿这个去做同屏联动屏幕上的人嘴巴都对不上基本没法用。RTSP 比 RTMP 强一些延迟能压到几百毫秒而且抓包、调试都方便。但 RTSP 本质上还是一对一的会话20 个终端同时看发送端或者流媒体服务器就得发 20 路单播流又是一堆要部署、要调优的中间件。WebRTC 延迟最低能到几十毫秒效果确实顶。但它对信令服务器、STUN/TURN 的要求很高纯内网场景下这些协商机制反而成了多余的复杂度而且终端兼容性需要专门的前端配合不是每个播放器都能直接吃。剩下就是我最终选的路UDP 组播。组播和单播的最大区别在于发送端只往组播地址发一份数据交换机会根据接收端的 IGMP 加入情况自动把这份数据复制给所有订阅了该组播组的终端。接收端不管有多少个发送端的负载都不变网络里也只有一份流在走效率极高。那编码为什么用 H264HEVC、AV1 在同等画质下码率确实更低但硬解兼容性差很多老终端、工控机、瘦客户端都不支持而且编码开销大实时性反而不如 H264 稳。H264 是几乎所有设备都认识的格式Profile 选 main 或者 high连很多年前的硬件都能硬解。局域网带宽普遍不紧张H264 是延迟、画质、兼容性三者之间最稳的平衡点。所以整套架构就是OBS 采集 → FFmpeg 编码 → RTP/H264 组播 → 终端各自解码播放链路里没有任何中转服务器也没有二次转封装。1.2 OBS 和 FFmpeg 的分工与衔接方式很多刚接触的人有个误区觉得既然用 FFmpeg那就干脆全用 FFmpeg用 OBS 就全程 OBS。实际上这两个工具最强的地方根本不在一个维度。OBS 强在“采集和合成”。窗口捕获、显示器捕获、摄像头、图片、文字、滤镜、场景切换这些操作在 OBS 里几秒钟就能搞定还自带虚拟摄像头、NDI 这类输出接口。FFmpeg 强在“编码和传输”。编码参数的控制粒度极细支持的输出协议极其丰富甚至可以一句话直接写 RTP 组播这是 OBS 图形界面做不到的。所以我的分工很明确OBS 只负责把画面“喂”给系统FFmpeg 再从这个系统接口取走画面去编码和发送。两者之间的胶水最常用的就是虚拟摄像头。OBS 从 28 版本开始内置了虚拟摄像头功能Windows、Linux、macOS 都能用。OBS 把画面推给虚拟摄像头FFmpeg 从虚拟摄像头采集之后的事情就全是 FFmpeg 的活了。有人会问OBS 的高级输出模式里不是自带 FFmpeg 自定义输出吗能不能直接填一个rtp://239.255.1.1:5004让 OBS 自己发组播答案是可以试验但我不推荐作为主路径。OBS 的自定义 FFmpeg 输出对格式参数的支持比较有限调试日志也不直观而且不同平台构建版本的行为有差异。用虚拟摄像头解耦之后改编码参数、改组播地址、改端口都不需要动 OBS两边可以各自独立调试出问题也容易定位。2. 环境准备与 H264 组播参数2.1 安装 FFmpeg 和启用 OBS 虚拟摄像头Windows 下装 FFmpeg 我一般直接到官网下载 essentials build 的 zip 包解压到C:\ffmpeg然后把C:\ffmpeg\bin加进系统 PATH再开一个命令行窗口执行ffmpeg -version验证。这里有个高频坑解压完执行 ffmpeg提示“无法将‘ffmpeg’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”十有八九是 PATH 没加对或者改完 PATH 之后没重新打开终端窗口。Windows 的 PATH 环境变量修改后新开终端才会生效这是个特别容易忽略的细节。Linux 下安装比较简单Ubuntu/Debian 系直接apt install ffmpegCentOS/RHEL 系建议用 EPEL 加 rpmfusion 仓库或者用 FFmpeg 官方提供的静态构建包。装完同样跑ffmpeg -version确认编译选项里带了--enable-libx264和--enable-librtmp。最小化安装的发行版很可能不带 libx264可以用ffmpeg -encoders | grep libx264验证没有的话就得补装编码器。OBS 这边28 版本之后在“控件”面板直接有“启动虚拟摄像头”按钮点一下就会注册一个虚拟设备。Windows 下设备名叫“OBS Virtual Camera”Linux 下通常对应/dev/video0这种 v4l2 节点macOS 下对应一个 avfoundation 设备。启动之前要先确认 OBS 场景里真的有画面否则虚拟摄像头输出的就是黑屏。如果设备列表里找不到 OBS 虚拟摄像头大概率是驱动没加载或者 OBS 版本太旧升级到最新版基本能解决。2.2 H264 低延迟编码参数这块是整个链路的核心。直接拿 FFmpeg 默认的 H264 参数去编码然后走 RTP延迟会明显偏高主要原因是 x264 为了压缩率会引入前瞻帧缓存和 B 帧重排。低延迟串流的编码参数要围绕一个原则来设让编码器“看到一帧就立刻吐出一帧”不要积累任何形式的缓冲。我常用的发送端命令模板长这样ffmpeg -re -i input.mp4 -c:v libx264 \ -preset ultrafast \ -tune zerolatency \ -profile:v high \ -g 25 \ -bf 0 \ -sc_threshold 0 \ -b:v 4M -maxrate 4M -bufsize 8M \ -an \ -f rtp rtp://239.255.1.1:5004逐个参数说清楚为什么这么设。preset控制编码速度和压缩率的权衡ultrafast牺牲一点压缩率换来极低的编码延迟和 CPU 占用。局域网带宽宽裕的时候这个取舍完全值得。tunezerolatency是 x264 专门为低延迟场景准备的调优模式它会关闭帧级并行化里引入的前瞻缓冲并强制按输入顺序输出编码帧。这个参数是低延迟的关键。没有它即使 preset 已经用了 ultrafast编码器也可能因为内部缓存多憋几帧延迟一下就多出几十甚至上百毫秒。g 25是关键帧间隔也就是每隔 25 帧设一个 IDR 关键帧。这对组播场景尤其重要因为接收端加入组播组时必须先收到一个关键帧才能完整解码出画面。如果这个值设成 250新加入的终端最坏要等 10 秒才能出画面体验极差。按 25fps 算25 帧就是 1 秒一个 IDR等待时间可接受。如果对“秒开”要求更高可以设成 10 或 12但码率会稍微涨一点。bf 0是关闭 B 帧。B 帧的解码依赖前后的 P 帧和 I 帧解码器必须等后续帧到达才能把 B 帧解出来这不是我们想要的低延迟行为直接关掉。sc_threshold 0是关闭场景切换检测避免编码器在画面剧烈变化时擅自插入关键帧造成码率瞬时的波动。组播场景下码率越平稳越好。最后是码率控制。组播里我建议用 CBR 风格b:v设目标码率maxrate和bufsize把码率波动限制住。4M 是 1080p30 在局域网一个比较稳妥的档位画质和带宽都兼顾。bufsize不要设太大控制在两个 GOP 的大小以内不然编码器会为了平滑码率积累太多缓冲延迟就上去了。2.3 组播网络配置与防火墙组播要用 IPv4 组播地址范围是 224.0.0.0 到 239.255.255.255。我习惯用239.0.0.0/8这是本地管理范围的组播地址不会被公网路由器转发适合内网使用。测试时我固定用239.255.1.1端口用 5004 或 1234避开常用服务端口。接收端工具对端口也有要求VLC 对某些高位动态 RTP 端口识别不太好所以统一规划地址和端口多路信号才不会乱。网络层面有几件事必须确认。第一交换机的 IGMP snooping 要开启。如果交换机不支持 IGMP snooping 或功能被关了组播数据就会被当作广播发给所有端口几十个端口同时收 4M 流带宽浪费非常明显。普通千兆管理型交换机基本都支持登录管理页面确认一下即可。第二TTL 设置要合理。内网发送 TTL 设 1 或 2 就够防止数据包被三层设备转发到其他网段。FFmpeg 的 RTP 输出可以通过 URL 参数设置比如rtp://239.255.1.1:5004?ttl1。只有跨 VLAN 分发时才需要调大 TTL并配合路由器的组播路由配置。第三多网卡机器要绑定源地址。FFmpeg 的 RTP muxer 支持localaddr选项例如rtp://239.255.1.1:5004?localaddr192.168.1.10ttl1确保组播包从正确的网卡出去。防火墙是另一个高频坑。Windows 上第一次运行 FFmpeg 发送 UDP 组播时系统会弹窗询问是否允许访问网络如果当时点了取消之后接收端永远收不到包。授权清理可以用管理员权限执行netsh advfirewall firewall delete rule namertp-mcast然后重新运行 FFmpeg弹窗时点“允许”。Linux 下如果开启了 firewalld 或者 ufw也要放行对应的 UDP 端口例如ufw allow 5004/udp。3. 完整链路配置与实测3.1 推荐架构OBS 虚拟摄像头 FFmpeg这套链路我日常用得最多。第一步打开 OBS建好场景和来源。为了低延迟场景里不要堆太多滤镜和复杂转场一个视频采集设备加一个文字来源已经足够。然后点“控件 → 启动虚拟摄像头”看到状态变成运行中OBS 这半边就绪了。Windows 下可以打开系统相机应用选择“OBS Virtual Camera”预览确认画面正常。第二步让 FFmpeg 能认出虚拟摄像头。Windows 下先执行ffmpeg -f dshow -list_devices true -i dummy如果输出里能看到OBS Virtual Camera就说明设备可被访问。列不出来的时候检查 OBS 虚拟摄像头是否启动或者用管理员身份运行终端再试一次。第三步启动发送端ffmpeg -f dshow -i videoOBS Virtual Camera \ -c:v libx264 -preset ultrafast -tune zerolatency \ -profile:v high -g 25 -bf 0 -sc_threshold 0 \ -b:v 4M -maxrate 4M -bufsize 8M \ -an -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdp注意这里没有加-re这点特别关键。-re的作用是让 FFmpeg 按原始帧率慢速读取输入适用于把本地文件当成直播源模拟推流。但从虚拟摄像头、摄像头采集时输入本身就是实时产生的数据流再加-re会让 FFmpeg 去匹配一个不存在的“1x 速度”轻则等待重则丢帧卡顿。第四条命令里我加了-sdp_file live.sdp。RTP 裸流不像 RTMP 那样自带会话描述接收端解码之前必须知道编码格式、负载类型、时间戳频率和 SSRC 信息这些都要通过 SDP 文件来告知。FFmpeg 输出 RTP 时默认会把 SDP 打印到 stderr用-sdp_file参数则可以保存成文件便于分发给所有接收端。这个文件只要生成一次所有终端共用一份。3.2 无 OBS 场景纯 FFmpeg 屏幕或设备捕获有些场景根本不需要 OBS 的合成能力比如监控一台无人值守的电脑屏幕、把一个 USB 摄像头画面推给多块大屏。这时候用 FFmpeg 一步到位更干净。Windows 下捕获整个桌面ffmpeg -f gdigrab -framerate 30 -video_size 1920x1080 -i desktop \ -c:v libx264 -preset ultrafast -tune zerolatency \ -profile:v high -g 30 -bf 0 \ -b:v 5M -maxrate 5M -bufsize 10M \ -an -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdpgdigrab 是 Windows 下最简单直接的桌面采集方式但需要注意它依赖物理桌面会话如果通过服务方式在后台跑可能采不到画面。Linux 下对应的是 x11grabffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i :0.0 \ -c:v libx264 -preset ultrafast -tune zerolatency \ -profile:v high -g 30 -bf 0 \ -b:v 5M -maxrate 5M -bufsize 10M \ -an -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdpx11grab 的输入格式是 DISPLAY 形式比如:0.0。如果在 SSH 会话里运行要确认当前用户能访问 X server否则会报权限错误。如果是 USB 摄像头直采Linux 下可以用 v4l2 输入ffmpeg -f v4l2 -input_format h264 -video_size 1920x1080 -framerate 30 -i /dev/video0 \ -c:v copy -an -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdp很多 USB 摄像头自带硬件 H264 编码这时可以用-c:v copy直接复制原始码流不需要 CPU 重新编码延迟和 CPU 占用都极低。但前提是摄像头驱动支持-input_format h264很多设备实际只输出 YUYV 或 MJPG这种情况还是要交给 libx264。3.3 已有流或文件转组播的常用方法如果本地已经有一路 RTSP 摄像头想把这路画面以组播方式发给多个终端可以直接拉流再发ffmpeg -rtsp_transport tcp -i rtsp://192.168.1.20:554/stream1 \ -c:v copy -an -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdp摄像头输出的往往已经是 H264直接 copy 转封装就能组播CPU 占用可以忽略。这里需要注意来源传输模式RTSP 默认走 UDP弱网下容易花屏-rtsp_transport tcp可以提高稳定性。代价是 TCP 传输本身可能引入几十到几百毫秒的延迟但比重新编码简单得多。想循环播放一个本地视频文件比如餐厅里的大屏宣传片ffmpeg -stream_loop -1 -re -i promo.mp4 \ -c:v copy -an -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdp这里必须加-re因为文件本身没有实时节奏不加-re会把整段文件以最高速度瞬间发完画面直接飞掉。-re让它按照文件里的时间戳以 1x 速度播放同时-stream_loop -1实现无缝循环。3.4 接收端配置FFplay、VLC 以及终端兼容性接收端最容易出问题的点就是“没等到关键帧”。组播是 UDP 无连接传输新加入的接收端如果没赶上 IDR 关键帧就要等下一个关键帧才能出画面。前面把 GOP 设成 25 或 30 帧意味着最多等 1 秒左右体验是可以接受的。FFplay 播放最直接指定 SDP 文件即可ffplay -protocol_whitelist file,udp,rtp,tcp -fflags nobuffer -flags low_delay live.sdp-protocol_whitelist是为了让 FFplay 信任文件、UDP、RTP 这几个协议避免安全限制导致的加载失败。-fflags nobuffer和-flags low_delay是低延迟播放的关键FFplay 会尽量减少缓冲画面可能在网络抖动时出现撕裂但内网一般稳定问题不大。VLC 打开组播流时建议直接打开 SDP 文件而不是填rtp://239.255.1.1:5004地址。SDP 文件里描述了载荷类型和编码格式VLC 才知道按什么格式解码。直接填 RTP 地址时某些版本会尝试按 MPEG-TS 解析黑屏概率很高。手机端的 VLC for Mobile 同样支持直接从共享目录打开 SDP 文件适合临时测试。如果是程序化接收Python 里可以用av库读取 SDP或者用 GStreamer 的udpsrc加rtph264depay接avdec_h264。这种情况下接收端要自行完成 RTP 解包和 H264 解码可定制性更强适合做多路画面拼接和信号监测。4. 常见问题、延迟调优与踩坑心得4.1 延迟到底消耗在哪先说实测数据。在同一个千兆交换机下OBS 虚拟摄像头进 FFmpegultrafast加zerolatency编码 1080p30 的 4M 流FFplay 端到端延迟大约在 80 到 150 毫秒之间这个数据在局域网多点分发场景里非常够用。如果换成 RTMP即使本地跑 nginx-rtmp延迟也至少 300 毫秒起步。差距主要来自三块。第一块是编码缓冲。x264 不开zerolatency时为了压缩率会缓存几帧再输出每帧约 33 毫秒缓存 5 帧就是 160 毫秒所以tunezerolatency必须开。第二块是网络缓冲。UDP 组播本身没有重传交换机转发延迟在微秒级基本可以忽略但如果接收端播放器默认缓冲设得大会把收到的包攒够队列再抛给解码器很容易吃掉几百毫秒。所以接收端要加nobuffer。第三块是解码和渲染。硬解延迟通常低于软解终端性能一般时软解一帧 1080p 也可能花 20 到 30 毫秒。要量化延迟最笨也最有效的方法是把摄像头对准一个毫秒级计时器或者手动秒表然后接收端对着屏幕拍一张照对比拍摄时间与画面里计时器的差值。这个方法没有任何额外开销比任何工具都好使我每次调优都是这么干的。4.2 常见问题速查表这里整理一份我在实际项目中遇到的典型问题可以直接对照排查。现象可能原因解决办法FFplay 打开 SDP 黑屏没等到关键帧 / payload 类型不匹配等待 1 秒看是否恢复检查 gop 是否过大确认 SDP 与编码参数一致接收端收不到任何包防火墙拦截 / 组播地址网段不对 / 交换机 IGMP snooping 未开接收端抓包确认是否到达放行 UDP 端口检查交换机配置OBS 虚拟摄像头输出黑屏场景里没有来源或未启动虚拟摄像头在 OBS 主界面确认有画面重新启动虚拟摄像头编码卡顿、丢帧严重CPU 不足 / 分辨率帧率过高降为 720p30用 ultrafast启用硬件编码画面花屏UDP 丢包 / 接收端缓冲太小检查交换机端口和网线接收端稍微增大缓冲FFmpeg 报 “Protocol rtp not supported”构建版本太老或被裁剪换官方完整构建执行ffmpeg -protocols确认有 rtp组播跨网段不通三层路由未配置 / TTL 太小增加?ttl2或配置组播路由考虑用单播中继4.3 几个值得试的进阶技巧硬件编码可以大幅降低 CPU 占用。NVIDIA 显卡用-c:v h264_nvenc -preset p6 -tune llIntel 核显用-c:v h264_qsv -preset veryfast。硬件编码在同等码率下画质可能略逊于 x264但延迟和 CPU 占用优势明显适合单机多路并发发送。启用前先确认驱动和 FFmpeg 构建支持对应编码器用ffmpeg -encoders | grep nvenc可以验证。音频也可以放到同一路组播里。前面命令大多用-an关掉了音频要带音频就同时指定视频和音频输入设备ffmpeg -f dshow -i videoOBS Virtual Camera:audio麦克风 (USB Audio) \ -c:v libx264 -preset ultrafast -tune zerolatency \ -b:v 4M -c:a aac -b:a 128k \ -f rtp rtp://239.255.1.1:5004?ttl1 -sdp_file live.sdp接收端的 SDP 文件会同时包含音频和视频的 m 行FFplay 会自动解码。注意音频编码会增加一点延迟追求极致低延迟的场景可以只传视频音频用独立通道。对于长期运行的服务场景建议加-loglevel warning降低日志输出量避免 FFmpeg 默认的 info 级别日志刷屏。想要更细的监控可以用-progress pipe:1把编码进度输出到管道配合脚本盯码率和丢帧情况。发送端的实时状态也能反映很多问题比如 drop 数量持续增长通常意味着输入卡顿或编码跟不上。4.4 我反复踩过的几个坑第一个坑是给实时采集加-re。前面说过从虚拟摄像头或摄像头采流时加-re实际上等于强制要求输入按文件 1x 速度播放采集设备本来就是实时产生帧的-re反而成了瓶颈表现就是画面一顿一顿甚至直接卡死。判断方法很简单输入源是实时设备就不要加-re输入源是文件或图片序列才需要加。第二个坑是 SDP 文件丢失。RTP 裸流没有 RTSP 那样的控制通道播放器不知道编码格式时只能靠 SDP 文件来猜。有一阵我把 SDP 生成在临时目录重启机器后忘了备份所有接收端一起起不来。后来我每次启动都用-sdp_file指定到一个固定路径比如/opt/live/live.sdp接收端直接引用这个固定路径再也不怕文件跑丢。第三个坑是防火墙弹出窗口误点。Windows 上第一次跑 FFmpeg 出站 UDP弹窗点“取消”之后后面很长一段时间收端都收不到包。检查的时候还一度怀疑是交换机和网卡的问题最后才发现是 Windows 防火墙把 FFmpeg 出方向拦了。用管理员权限清理规则之后重新授权问题立刻消失。第四个坑是组播包被交换机当广播放。部分老交换机或者某些网卡驱动会把组播泛洪到所有端口小规模场景感觉不明显几十个端口同时收 4M 的流带宽就开始吃紧。排查方式是用 tcpdump 在接收端抓包如果没订阅组播的端口也收到了数据就要去查交换机的 IGMP snooping 是否真的生效。最后分享一个我现在固定的用法。很多项目里我并不只发一路组播而是用 OBS 做一套场景喂给 FFmpeg同时开两到三路不同码率、不同组播地址的流一路给大屏一路给移动端一路给录像服务器。发送端只有一台机器编码压力大的时候就上 N 卡硬件编码性能足的机器用 x264。这套架构最让我满意的是每一层都足够独立OBS 场景随便改编码参数随便调组播地址随便换排查问题特别干净。低延迟直播这件事方案不是越复杂越好而是每一层只做一个事层与层之间的接口越简单越好。链路干净了问题就少了一半真出问题也别慌从发送端到接收端一层一层抓包看组播的调试其实比 TCP 协议栈那些黑盒链路好查得多。