☰
Node.js + FFmpeg + flv.js 低延迟直播搭建全攻略
2026/10/20 18:14:17 网站建设 项目流程

如果你最近有类似需求:给一场运营活动搭一套低延迟直播、给线上课程做实时流播放,或者只是朋友之间想体验一把从推流到拉流的完整链路,那么Node.js + FFmpeg + flv.js这个组合,绝对是你目前能找到的最省事的全栈直播方案。我最近刚帮某运营活动跑通了一条这样的链路,从安装环境到手机浏览器稳定播放,一个下午搞定。这篇文章把我踩过的坑、调过的参数、试过的方案对比全部写出来,给想快速落地的人一个可以直接抄作业的参考。

先说结论:这套方案的核心思路是,用 FFmpeg 负责采集和编码,用 Node.js 生态里的开源流媒体服务器负责接收 RTMP 流并分发 HTTP-FLV 流,最后在浏览器端用 flv.js 把流解码播放出来。整个过程全部可控、可持续扩展,而且不依赖云厂商的直播服务,适合自建、私有化、低成本验证的场景。

1. 方案选型:为什么是 HTTP-FLV,而不是 HLS 或 WebRTC

1.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 以上版本,我目前用的是 18,node-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 下截取整个桌面,用 gdigrab:

ffmpeg -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 video="USB Camera":audio="Microphone" \ -c:v libx264 -tune zerolatency -preset veryfast -g 60 -c:a aac \ -f flv rtmp://127.0.0.1:1935/live/camera

macOS 下用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 lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>直播播放器</title> </head> <body style="background: #000; margin: 0;"> <video id="video" controls muted autoplay style="width: 100%; max-height: 100vh;"></video> <script src="https://cdn.jsdelivr.net/npm/flv.js@1.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?sign=mysecret

只有携带正确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% 的问题:

  1. 用浏览器直接访问 HTTP-FLV 地址,比如http://127.0.0.1:8000/live/demo.flv。如果页面空白但数据在下载,说明流服务是好的,问题出在播放器或跨域;如果直接 404,说明推流没成功或流名不对。
  2. 用 VLC 播放 RTMP 地址,确认推流链路是否正常。如果 VLC 也打不开,回到 FFmpeg 推流端看日志。
  3. 看浏览器控制台报错。如果提示 CORS 相关错误,优先检查allow_origins配置;如果提示fatal错误,多半是流格式或解码器问题。
  4. 检查推流命令是否持续在跑。很多新手因为没加-re,FFmpeg 瞬间推完就退出了,等到浏览器去看时流已经断了。

这个顺序,每次都能节约大量排查时间。

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 anullsrc=r=44100:cl=stereo

实测加上静音音轨后,播放器稳定性提升非常明显。

6.4 Safari 与移动端的兼容处理

老实说,flv.js 最大的短板就是 Safari 和 iOS 上的 WebView 不支持 MSE 播放 FLV。解决方案没有魔法,只能做 HLS 回退:让服务端同时开启 HLS 输出,前端在检测到不支持 flv.js 时切换为<video src="xxx.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属性。这是浏览器平台的硬性限制,不是代码能绕过的。


最后分享一点我个人在实操中的体会:这套方案之所以适合"快速直播",核心在于它的每一层都有成熟的现成工具,且都是配置导向而不是开发导向。对于需要快速验证直播业务的团队,我的建议是先按文中的默认参数完整跑通一遍,再针对你的实际网络环境逐步调优延迟。遇到问题先怀疑链路,再怀疑配置,最后怀疑代码。这套排查思路,比任何单一参数都能帮你走得更远。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询