Node.js + FFmpeg + flv.js 低延迟直播搭建全攻略

发布时间:2026/10/20 18:14:32
Node.js + FFmpeg + flv.js 低延迟直播搭建全攻略
如果你最近有类似需求给一场运营活动搭一套低延迟直播、给线上课程做实时流播放或者只是朋友之间想体验一把从推流到拉流的完整链路那么Node.js FFmpeg flv.js这个组合绝对是你目前能找到的最省事的全栈直播方案。我最近刚帮某运营活动跑通了一条这样的链路从安装环境到手机浏览器稳定播放一个下午搞定。这篇文章把我踩过的坑、调过的参数、试过的方案对比全部写出来给想快速落地的人一个可以直接抄作业的参考。先说结论这套方案的核心思路是用 FFmpeg 负责采集和编码用 Node.js 生态里的开源流媒体服务器负责接收 RTMP 流并分发 HTTP-FLV 流最后在浏览器端用 flv.js 把流解码播放出来。整个过程全部可控、可持续扩展而且不依赖云厂商的直播服务适合自建、私有化、低成本验证的场景。1. 方案选型为什么是 HTTP-FLV而不是 HLS 或 WebRTC1.1 三种直播协议的现实对比做直播方案第一个绕不开的选择就是协议。市面上常见的浏览器直播协议无非三种HLS、HTTP-FLV、WebRTC。我用一张表把这几个关键差异列出来方便你对照。协议延迟范围浏览器兼容性服务端复杂度典型场景HLS6~30 秒原生支持兼容性最好需要对流进行切片较复杂点播、大型赛事、弱网环境HTTP-FLV1~5 秒依赖 flv.js 或类似 MSE 方案实现简单流式转发即可直播互动、课程直播WebRTC200~800 毫秒原生支持但信令与服务端架构复杂需要 SFU/MCU 服务器运维成本高视频连麦、实时互动当初我最先考虑的是 HLS因为浏览器直接支持不需要引入任何播放器库。但实测下来HLS 的切片机制导致延迟通常在十秒以上对于需要主持人和观众互动的场景来说完全不能忍。WebRTC 延迟虽然最低但服务端要处理信令、ICE、媒体协商自己搭一套成本非常高属于杀鸡用牛刀。HTTP-FLV 的好处在于它本质上就是通过 HTTP 协议传输 FLV 格式的媒体流。服务端实现一个持续输出的响应流客户端用 flv.js 把数据接住再通过浏览器原生支持的 MediaSource Extensions 做解封装和喂帧。既有 HLS 的走 HTTP 就好的便利又把延迟压到了 1~3 秒非常适合快速直播这个场景。1.2 Node.js FFmpeg flv.js 各自承担什么角色三条产品线都有成熟工具为什么一定要是 Node.js FFmpeg flv.js我实际用下来核心原因是这套组合的分工非常清晰而且都是各自领域里开箱即用的解决方案。FFmpeg 是推流端的万能工具。无论你的视频源是本地文件、摄像头、桌面屏幕还是一个来自其他协议的直播流FFmpeg 都能统一采集、编码、封装成 RTMP 流推出去。Node.js 生态里的流媒体服务是分发端。基于 Node.js 的 node-media-server 这类开源项目天然支持 RTMP 推流接入和 HTTP-FLV 输出。更重要的是它和你的业务后端是同一个语言体系做鉴权、房间管理、状态统计时可以直接集成不用在 C/C 服务和 Node.js 服务之间来回横跳。flv.js 是播放端的桥梁。它把 HTTP-FLV 流和浏览器原生播放能力打通让 Chrome、Firefox、安卓端 WebView 都能直接播放而且提供了细粒度的参数来控制缓冲和延迟。这套组合还有一个隐藏优势全部都是纯开源、跨平台的工具链Windows、macOS、Linux 下表现一致。对于需要快速验证产品、上演示项目或内部活动的团队来说是性价比最高的选择。2. 环境准备与服务端搭建2.1 准备工作清单在开始之前我建议先花十分钟把环境检查好。这个环节跳过任何一步后面排错都会很痛苦。需要准备三样东西第一Node.js 环境。建议使用 14 以上版本我目前用的是 18node-media-server 跑起来没有任何问题。如果你用的是老版本 Node.js可能会遇到依赖模块不兼容的问题尤其是加密相关模块的 API 改动。第二FFmpeg。这是最容易踩坑的地方。Windows 上下载 FFmpeg 时一定要选择包含 libx264 编码器的完整版不要图省事下载精简版。我在初期使用精简版时推流命令直接报Unknown encoder libx264排查了好久才意识到是能力集的问题。安装完成后先在终端跑一下ffmpeg -version确认输出中有--enable-libx264这个标识否则后续视频流推不上去。第三一个可用的 HTTP 静态服务。其实这个不是必须的但后续播放器页面总要有地方放。直接用 Flask 临时起一个、用 Nginx 托管或者干脆在项目里用 Express 顺便提供静态文件服务都可以。2.2 用 Node.js 拉起流媒体服务先用 npm 初始化项目并安装流媒体依赖mkdir live-demo cd live-demo npm init -y npm install node-media-server express然后新建一个server.js写入核心配置const NodeMediaServer require(node-media-server); const config { rtmp: { port: 1935, chunk_size: 60000, gop_cache: true, ping: 30, ping_timeout: 60 }, http: { port: 8000, allow_origins: * } }; const nms new NodeMediaServer(config); nms.run();解释一下这里配置的关键点rtmp.port是推流端要连接的端口默认 1935 是 RTMP 的通用端口。chunk_size是 RTMP 块大小默认 60000 字节足够不用乱改。gop_cache决定服务端是否缓存 GOP即关键帧之间的完整数据。开着的效果是新观众进房间秒出画面因为可以直接从最近的关键帧开始播放代价是延迟会略高一点。调试阶段建议开着正式追求低延迟时再关闭。http.port是 HTTP-FLV 的访问端口播放器就是从http://服务器IP:8000/live/名称.flv拉流的。allow_origins: *是为了避免浏览器跨域问题。如果不设置flv.js 在页面上请求流时可能会被浏览器的 CORS 策略挡住。运行服务node server.js服务起来后默认会监听 1935 和 8000 两个端口。可以先不管别的直接测试链路通不通。2.3 第一次推流与播放验证链路是否打通最好在最早期就确认避免后面堆了一堆代码再去排查为什么页面黑屏。找一个测试视频文件执行以下 FFmpeg 推流命令ffmpeg -re -stream_loop -1 -i test.mp4 -c:v libx264 -profile:v baseline -level:v 3.1 -pix_fmt yuv420p -tune zerolatency -preset veryfast -g 60 -c:a aac -ar 44100 -b:a 128k -f flv rtmp://127.0.0.1:1935/live/demo推流成功后终端会持续滚动帧率和码率信息。这时可以先用 VLC 打开rtmp://127.0.0.1:1935/live/demo如果 VLC 能正常出画面说明 RTMP 链路已经通了。然后再打开浏览器访问http://127.0.0.1:8000/live/demo.flv正常情况下浏览器会提示下载或显示一些乱码内容这都说明 HTTP-FLV 分发正常。这个先验证链路、再写代码的顺序非常重要。我做过的多次实操中几乎所有问题都可以归因到链路还没通就开始调播放器。3. FFmpeg 推流的正确姿势3.1 文件推流最基础的链路FFmpeg 推流看起来就是一行命令但每一个参数都对最终播放体验有直接影响。我把上一节那条命令拆开逐个讲这样才能做到能复现、能调整。ffmpeg -re -stream_loop -1 -i test.mp4 \ -c:v libx264 -profile:v baseline -level:v 3.1 -pix_fmt yuv420p \ -tune zerolatency -preset veryfast -g 60 \ -c:a aac -ar 44100 -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/demo-re是读取文件时的限速参数它让 FFmpeg 以视频原始帧率的速度读取文件而不是一股脑地推完。少了这个参数本地文件会被瞬间推完播放器看到的流会像开了 100 倍速无法正常播放。-stream_loop -1表示无限循环播放这个文件。做 7x24 小时无人值守直播时非常有用。-c:v libx264指定视频编码器。直播推流必须选编码器不能像转存文件那样直接-c copy因为 RTMP 对 H.264 的封装格式有严格要求。-profile:v baseline是 H.264 编码档次。Baseline 档对低端设备和浏览器解码器的兼容性最好而且在直播场景下延迟低于 High 档。画质有轻微损失但在大多数直播场景里看不出差别。-tune zerolatency是 x264 的调优参数专门为低延迟场景设计。它会禁用一些缓冲和预取机制让每一帧数据尽快编码输出。实测开启后延迟能下降 300~500 毫秒相当明显。-g 60设置关键帧间隔为 60 帧。按照常见的 30fps 帧率代表每 2 秒一个关键帧。这个值既保证播放器能较快起播又不会让码率暴涨。完整的推流参数我建议做成一个脚本或配置文件方便后续复用。3.2 采集屏幕和摄像头推流直播源不一定是文件屏幕分享和摄像头采集在实际业务里更常见。FFmpeg 在这两种场景下的用法不太一样值得单独列出来。Windows 下截取整个桌面用 gdigrabffmpeg -f gdigrab -framerate 30 -i desktop -c:v libx264 -tune zerolatency -preset veryfast -g 60 -c:a aac -f flv rtmp://127.0.0.1:1935/live/screen截取指定窗口时把-i desktop换成-i title窗口标题即可。摄像头采集在 Windows 下通过 dshow 实现。先用以下命令列出可用设备名ffmpeg -list_devices true -f dshow -i dummy拿到设备名后写成-f dshow -i video摄像头名称:audio麦克风名称。一个完整的摄像头推流命令示例ffmpeg -f dshow -i videoUSB Camera:audioMicrophone \ -c:v libx264 -tune zerolatency -preset veryfast -g 60 -c:a aac \ -f flv rtmp://127.0.0.1:1935/live/cameramacOS 下用avfoundation作为采集接口Linux 下用x11grab。套路都类似建议提前写个小脚本缓存设备列表避免每次手敲。3.3 延迟与码率之间的取舍关于延迟我总结出三个元凶和对应的解法第一个元凶是 B 帧。x264 默认会开启 B 帧作为参考帧这在一定程度上能提升压缩效率但也引入了额外的帧级延迟。追求低延迟时可以在命令里补充-bf 0把 B 帧彻底关掉。这个参数在高码率直播中效果显著画面质量几乎无感知变化。第二个元凶是 GOP 太大。播放器只有在收到关键帧的那一瞬间才能开始解码出画面。如果你的 GOP 是 120 帧4 秒那么观众进入后最坏要等 4 秒才看到画面。所以低延迟直播中-g一般控制在 24~60对应 1~2 秒同时播放端和服务端配合就能实现秒开 低延迟。第三个元凶是服务端的 GOP 缓存。node-media-server 的gop_cache字段默认开或者关取决于你设的配置。如果追求最低延迟可以关闭它。但要注意关闭后新进播放器的用户必须等到下一个关键帧才能出画面所以在观众体验上反而可能感觉卡了一下。我的建议是观众数量大、网络情况复杂的场景开着内部演示、延迟敏感的互动场景关掉。实际操作中我在同一场测试里对比过推流端开启zerolatency且-g 30播放端关闭缓冲整体从摄像头采集到手机屏幕显示的延迟稳定在 1~1.5 秒已经非常接近 WebRTC 的体验了。4. flv.js 前端播放器实战4.1 播放器接入与初始化前端这块核心就是一个 HTML 页面加一个 flv.js。我用一个最精简的完整页面示例来说明你 30 秒就能接入!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title直播播放器/title /head body stylebackground: #000; margin: 0; video idvideo controls muted autoplay stylewidth: 100%; max-height: 100vh;/video script srchttps://cdn.jsdelivr.net/npm/flv.js1.6.2/dist/flv.min.js/script script const video document.getElementById(video); if (flvjs.isSupported()) { const player flvjs.createPlayer({ type: flv, isLive: true, url: http://127.0.0.1:8000/live/demo.flv }, { enableStashBuffer: false, liveBufferLatencyChampion: true, autoCleanupSourceBuffer: true, enableWorker: true }); player.attachMediaElement(video); player.load(); player.play(); } /script /body /html注意几个细节flvjs.isSupported()在播放器初始化前必须先判断避免在不支持的浏览器里抛出异常。type: flv和isLive: true是直播流的固定值如果你以后要播放点播 FLV 文件isLive要改成false。muted属性在带自动播放需求的页面里很关键。在移动端和部分 PC 浏览器策略下带声音的自动播放几乎都会被拦截静音自动播放则可以通过。用户点击页面后再打开声音即可。4.2 降低播放延迟的配置flv.js 里面对延迟影响最大的两个配置项是enableStashBuffer和liveBufferLatencyChampion。默认情况下flv.js 会使用内部缓冲区积累数据这在网络抖动时可以保证播放流畅但也意味着延迟会越拉越大。把enableStashBuffer: false打开后播放器尽量不缓存数据到了就播延迟可以显著下降。实测中这个配置能减少 1~2 秒的延迟是必开项。liveBufferLatencyChampion是 flv.js 的实时追赶模式。它会动态调整播放速度当检测到缓冲数据过多时稍微加快播放速率把延迟追回来但人耳几乎感知不到声音变化。这个选项在直播场景强烈建议开启。还有一点容易被忽略enableWorker: true可以开启 flv.js 的 Web Worker 模式把 FLV 解封装放到后台线程执行避免主线程卡顿导致播放掉帧。在画面较大时效果明显。4.3 断线重连与容错处理直播链路中网络抖动、推流端重启都有可能让播放中断。flv.js 默认不会自动重连但我们完全可以用它的事件机制自己实现。player.on(flvjs.Events.ERROR, function(type, detail, info) { console.error(播放错误, type, detail, info); // 连接断开或媒体错误时尝试重连 if (info info.code) { reconnect(3); } }); function reconnect(maxRetry) { let retryCount 0; const timer setInterval(() { if (retryCount maxRetry || player.playing()) { clearInterval(timer); return; } retryCount; player.unload(); player.detachMediaElement(); player.attachMediaElement(video); player.load(); player.play(); }, 2000); }一个重要的经验重连时一定要先unload()再重新load()如果直接调用load()部分浏览器会残留上一次的 MediaSource 状态导致画面永久卡住。另外重连次数要设上限不能无限重试否则用户已经没在看页面还在背后不停发起请求白白浪费资源。5. 接业务需求鉴权与录制5.1 给直播加上推流鉴权裸奔的流媒体服务在公网环境几乎是邀请别人来推流。node-media-server 提供了鉴权钩子可以在推流和播放时校验密钥。配置里增加auth字段const config { rtmp: { port: 1935, chunk_size: 60000, gop_cache: true, ping: 30, ping_timeout: 60 }, http: { port: 8000, allow_origins: * }, auth: { play: true, publish: true, secret: 你的随机密钥 } };这个secret会作为推流 URL 里的一个参数。比如你的密钥是mysecret那么推流地址就要写成rtmp://127.0.0.1:1935/live/demo?signmysecret只有携带正确sign参数的推流端才能推上来。这个机制对接业务时非常实用每次创建直播房间后端就生成一个带随机sign的推流地址给导播或讲师如果发现有人盗推直接在服务端把密钥换掉就能踢掉所有非法推流。播放鉴权也可以靠这个机制但我更推荐在播放器页面前面加一层业务登录只有登录用户才能拿到页面代码和播放地址。这样比单纯在流地址上做校验更灵活。5.2 顺带把直播录制做了运营活动或课程直播经常需要留存回放。录制这件事FFmpeg 一行命令就能搞定不需要在 Node.js 里写复杂的逻辑。最简单的录制方案是在推流的同时另开一个 FFmpeg 进程作为录像机拉取 HTTP-FLV 流并转存成文件ffmpeg -i http://127.0.0.1:8000/live/demo.flv -c copy -f segment -segment_time 3600 -strftime 1 record/demo_%Y%m%d_%H%M%S.mp4解释一下-c copy表示不转码直接复制流数据CPU 占用几乎为零。-f segment -segment_time 3600表示按小时切分文件避免单个录制文件过大后续剪辑、上传都方便。-strftime 1允许文件名带时间戳。实测这种方式即使跑 24 小时也不会积累内存切割逻辑稳定。缺点是一旦推流中断再恢复会生成一个新的录制文件需要在业务端做好文件列表和回放拼接的逻辑。6. 常见问题与排障实录6.1 播放器黑屏最全的排查顺序黑屏是直播开发里最常遇到的问题而且大概率不是因为播放器写错了。我习惯按下面的顺序排查能快速定位 90% 的问题用浏览器直接访问 HTTP-FLV 地址比如http://127.0.0.1:8000/live/demo.flv。如果页面空白但数据在下载说明流服务是好的问题出在播放器或跨域如果直接 404说明推流没成功或流名不对。用 VLC 播放 RTMP 地址确认推流链路是否正常。如果 VLC 也打不开回到 FFmpeg 推流端看日志。看浏览器控制台报错。如果提示 CORS 相关错误优先检查allow_origins配置如果提示fatal错误多半是流格式或解码器问题。检查推流命令是否持续在跑。很多新手因为没加-reFFmpeg 瞬间推完就退出了等到浏览器去看时流已经断了。这个顺序每次都能节约大量排查时间。6.2 延迟从 3 秒降到 1 秒的操作清单如果你觉得延迟偏高按下面的清单逐个检查基本能做到从哪几步里找出瓶颈。推流端有没有加-tune zerolatency和-bf 0。-g是不是超过了 60。flv.js 的enableStashBuffer是否设置为false。liveBufferLatencyChampion是否开启。node-media-server 的gop_cache是否关闭这个在观众进入时影响最大。播放器页面到流服务器之间的网络跳数是否太多。如果是公网拉流尽量让播放页面和流服务器在同一个机房。我曾经在一个活动里把上述参数从默认值全部调到位后延迟从 4.5 秒直接降到 1.2 秒观众反馈几乎没有延迟感。6.3 音画不同步和没有声音音画不同步通常由两个原因造成一是推流端输入的源本身不同步这个只能换源二是音频编码参数和设备不匹配。我在摄像头采集时遇到过麦克风采样率是 48000而推流参数写死 44100导致音视频时间戳错位。解法是让 FFmpeg 自动采样即移除-ar 44100让它跟随源设备。另一个常见问题是推流画面正常但完全没有声音。如果源设备没有音频输入FFmpeg 默认不会生成音轨而 flv.js 在遇到有视频无音频或音频流异常时可能会卡住画面。解决办法有两个一是确保推流命令里包含音频编码和音频设备二是如果确实没有音源可以生成一路静音音轨-f lavfi -i anullsrcr44100:clstereo实测加上静音音轨后播放器稳定性提升非常明显。6.4 Safari 与移动端的兼容处理老实说flv.js 最大的短板就是 Safari 和 iOS 上的 WebView 不支持 MSE 播放 FLV。解决方案没有魔法只能做 HLS 回退让服务端同时开启 HLS 输出前端在检测到不支持 flv.js 时切换为video srcxxx.m3u8。node-media-server 本身就支持 HLS 输出在配置里增加hls: { port: 8001, mode: save, path: ./hls }当推送名为demo的流时HLS 切片会生成到./hls/live/demo/目录播放地址就是http://127.0.0.1:8001/live/demo/index.m3u8。前端用特性检测决定走哪条路。另外提醒一下移动端自动播放的规则即使你用了 HLS 回退iOS 上也需要让用户点击一次播放按钮或者在 video 标签上同时设置muted和autoplay属性。这是浏览器平台的硬性限制不是代码能绕过的。最后分享一点我个人在实操中的体会这套方案之所以适合快速直播核心在于它的每一层都有成熟的现成工具且都是配置导向而不是开发导向。对于需要快速验证直播业务的团队我的建议是先按文中的默认参数完整跑通一遍再针对你的实际网络环境逐步调优延迟。遇到问题先怀疑链路再怀疑配置最后怀疑代码。这套排查思路比任何单一参数都能帮你走得更远。