Node.js + FFmpeg + flv.js:纯开源搭建浏览器直播系统
最近好多朋友问我如果手头没有云直播服务也没有商业级流媒体平台能不能用纯开源工具自己搭一套浏览器能直接看的直播我通常都会推荐 Node.js FFmpeg flv.js 这套组合。这套方案非常适合在局域网演示、内网监控、临时活动直播、产品原型验证这些场景里使用。它的核心思路很直接FFmpeg 负责采集视频源并推流Node.js 承担流媒体分发服务flv.js 负责在浏览器里把流拉下来并播放。整个链路没有商业闭源组件也没有复杂的会员订阅只要本机能装软件就能搭出一个延迟可控、画质可调的轻量直播系统。前端同学可以在这里学到 Socket 级别的流播放后端同学能接触流媒体服务端设计音视频入门者也能在 FFmpeg 参数里摸到编码世界的门道。我尽量把每一步都拆开讲包括参数为什么这么写、遇到问题怎么排查尽量帮大家少走弯路。1. 为什么选 Node.js FFmpeg flv.js 这一套1.1 直播链路的基本形态直播这件事本质上就是把一路连续的视频数据从采集端一路搬到播放端中间要经过编码、封装、传输、解封装、解码这几个环节。传统方案里RTMP 协议是推流端的主流选择Flash 时代则由 RTMP 直接打进浏览器播放。现在 Flash 已经离去浏览器端播放 RTMP 已经无路可走需要一种浏览器原生支持的协议来接住 RTMP 的流HTTP-FLV 就是目前兼容性和实时性综合表现不错的方案。HTTP-FLV 听名字就能理解它是把 FLV 数据块按照普通 HTTP 响应的方式一路传下去。播放器通过 HTTP 请求拿到一个持续的流不需要 WebSocket不需要插件现代浏览器配合 Media Source ExtensionsMSE就能逐段消费这些 FLV 数据。flv.js 正是把 FLV 流转换成浏览器可以喂给 video 元素的 ISO BMFF 片段从而实现低延迟播放。所以这套方案的标准链路长这样视频源摄像头、桌面画面、某个视频文件甚至手机摄像头。编码与推流FFmpeg 读取视频源进行 H.264 视频编码、AAC 音频编码封装成 FLV通过 RTMP 推给 Node.js 服务。流媒体分发Node.js 上跑一个 RTMP 接收服务同时对外以 HTTP-FLV 协议拉流给播放器。播放器浏览器里的 flv.js 接收 HTTP-FLV通过 MSE 喂给 video 标签播放。这个链路的好处是每一层都能单独替换。比如不想用 FFmpeg可以用 OBS 推流不想用自己写的服务端可以用 Nginx 的 RTMP 模块不想用 flv.js也可以留着 HTTP-FLV 给某些播放器内核直接播。整个架构没有过度绑定调试非常舒服。1.2 相比其他直播方案的取舍有人会问WebRTC 延迟更低为什么不用HLS 兼容性更好为什么不用这里要明确一点这套方案不是为了取代它们而是在特定场景下达到了一个极好的平衡。方案延迟浏览器兼容复杂度适合场景HTTP-FLV flv.js1-3 秒需支持 MSE 的浏览器低局域网直播、活动直播、监控画面HLS5-15 秒几乎全兼容低点播、延迟不敏感的直播WebRTC500 毫秒现代浏览器但信令复杂高视频会议、连麦、互动场景RTMP 直连播放低浏览器已不支持低已废弃仅推流端可用我在实际项目中用过 WebRTC 做过互动直播效果很好但信令服务、ICE 穿透、带宽控制这些足够写好几篇文章。如果只是想让一堆浏览器稳定地看摄像头画面没必要给自己上那么大的难度。HLS 虽然兼容性好但切片机制天然带来较高的延迟看监控画面还能接受搞互动就难受。HTTP-FLV 在两者之间拿捏得比较准延迟能压进两三秒实现难度又远远低于 WebRTC日常直播完全够用。这里要夸一下 Node.js 的生态。node-media-server 这个开源包本身就集成了 RTMP 接收和 HTTP-FLV 分发配置一下就能跑省去了自己解析 RTMP 包的痛苦。FFmpeg 是老牌音视频瑞士军刀几乎所有编码问题它都能处理。flv.js 则把浏览器播放的最后一公里铺平了。三样工具各自擅长各自的领域组合起来没有明显短板这也是推荐这套组合的最大理由。2. 环境准备与工具清单2.1 依赖安装动手之前先把基础环境装好。下面这些软件按自己的操作系统选择安装方式即可版本不用追最新稳定优先。第一Node.js。建议装 14 LTS 或更高版本因为 node-media-server 用到了比较新的语法老版本容易报一些莫名其妙的兼容错误。装完后在终端里输入 node -v 能看到版本号就说明没问题。第二FFmpeg。这是推流和转流的主力。不管是 Windows、macOS 还是 Linux 都能下载对应构建版本。安装完之后运行 ffmpeg -version出现版本信息即可。第三一个可以改的本机目录。我习惯建一个类似 live-demo 的目录里面分别放服务端代码、播放页面、后续可能用到的配置文件和日志。我的操作顺序是先把目录建好然后进入目录执行 npm init -y 生成 package.json这样后面安装依赖不会污染全局环境。如果你已经有一个公司项目或者测试项目也可以在这个项目里直接装只要端口不冲突就行。2.2 快速验证工具除了三个主力软件我还会准备两个辅助工具。一个是 OBS Studio。如果不想敲 FFmpeg 命令用 OBS 推流也是可以的它自带强大的场景采集能力适合临时把电脑桌面、窗口、摄像头混合成一路视频。当然如果你只打算用现成的视频文件做测试源那 OBS 就不是必需的。另一个是 VLC 播放器。VLC 可以作为第二双眼睛直接在桌面端拉 RTMP 或 HTTP-FLV 流验证推流到底成没成功。很多 flv.js 排不出的问题用 VLC 一测就能判断是服务端的问题还是浏览器端的问题。我自己的习惯是先用 VLC 确认流能播再打开浏览器调 flv.js这样能很自然地切分问题边界。如果你也这样操作遇到黑屏的时候就不会一头雾水了。3. 服务端搭建Node.js 媒体服务3.1 初始化项目与依赖这一步开始动真格。我在某个模拟环境里建了一个叫 live-demo 的项目目录然后安装 node-media-server。node-media-server 在 npm 上的包名是 node-media-server它提供了一套完整的流媒体服务支持 RTMP、HTTP-FLV、WebSocket-FLV、HLS 等协议输出。我们只需要用到它的 RTMP 接收和 HTTP-FLV 分发其他功能按需打开就行。安装命令mkdir live-demo cd live-demo npm init -y npm install node-media-server安装完成后node_modules 目录里会出现它以及相关依赖。如果你网络不太好可以换用 npm 镜像源这个属于常规操作不展开说了。3.2 编写媒体服务主程序在项目根目录下创建 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: 8100, allow_origin: * } }; const nms new NodeMediaServer(config); nms.run();这段配置看着简单每个参数背后都有讲究rtmp.port 是 RTMP 推流入口默认 1935FFmpeg 推流时就要把地址指向这里。rtmp.chunk_size 是 RTMP 分块大小。数值太大会增加单次传输的缓冲太小又会浪费 CPU。60000 是一个比较稳妥的中间值。rtmp.gop_cache 决定是否缓存关键帧。开启后新的播放端进入时可以更快拿到画面不会长时间黑屏。但代价是会增加一点延迟后续我们会讨论这个参数对延迟的影响。rtmp.ping 和 ping_timeout 是 RTMP 心跳检测服务端每隔 30 秒探测一次连接状态如果 60 秒内没有收到响应就断开连接。这样能自动清理那些推流端异常退出的僵尸连接。http.port 是 HTTP-FLV 的对外播放端口。我们设成 8100浏览器访问播放页面就是从这个端口拉流。http.allow_origin 设置跨域允许规则。默认 * 是允许所有来源正式环境建议限定为你自己的页面域名后面优化部分会给出安全写法。完成之后在终端运行node server.js控制台会输出 RTMP 服务监听 1935 端口、HTTP 服务监听 8100 端口的日志。看到这些服务端就站起来了。3.3 启动与验证服务端起了之后先别急着推流先确认端口是不是真的通了。可以用系统命令查看端口监听状态比如在 Linux 或 macOS 上执行lsof -i :1935 lsof -i :8100如果能看到 node 进程监听这两个端口说明服务已经就绪。很多人在这里会踩一个坑云服务器或者公司内网防火墙没有放行端口导致推流端能往内网推但外网播放器拉不到流。如果你是要跨机器测试记得在防火墙和安全组里把 1935 和 8100 都放行并确认本机 IP 不是 127.0.0.1否则从别的机器访问不到。4. FFmpeg 推流参数与实战4.1 推流命令拆解服务端准备好之后需要一路数据流进来。最稳定的方式就是用 FFmpeg 推流。下面这条命令是我在测试环境里最常用的一条ffmpeg -re -stream_loop -1 -i input.mp4 \ -c:v libx264 -preset veryfast -tune zerolatency \ -c:a aac -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/stream逐项解释一下-re 表示以源文件的原始帧率读取。不写这个参数FFmpeg 会一口气把所有帧都读出来变成类似快进的效果推流端瞬间就推完了根本起不到直播作用。-stream_loop -1 表示无限循环播放这个文件。测试时很好用它能让流一直存在方便反复调试播放器。正式直播时一般不写这个参数因为你要推的是实时采集源。-i input.mp4 指定输入文件。这个文件可以是 MP4、MKV 或者其他 FFmpeg 支持的格式建议先用一个画质稳定的短视频。-c:v libx264 把视频编码成 H.264。这是浏览器兼容性最好的编码也是 flv.js 能顺利解封装和解码的前提。-preset veryfast 用比较快的 x264 预设换取压缩效率。CPU 不太强的时候实时推流包veryfast 是很实际的选择。追求更高压缩比可以考虑 1080p 超清场景但要多关注 CPU 占用。-tune zerolatency 是给直播场景专用的调优参数减少编码器缓冲对降低延迟很有帮助。-c:a aac 把音频编码成 AAC 音频格式这是 FLV 封装里最通用的音频编码。-b:a 128k 设置音频码率。日常直播足够如果你音质要求高可以提到 192k。-f flv rtmp://127.0.0.1:1935/live/stream 指定输出封装格式为 FLV并推流到本地 RTMP 服务的 /live/stream 路径。这里的 /live/stream 是应用名和流名的组合。node-media-server 默认接收的应用名是 live后面的 stream 是流的唯一标识。如果推流时改了流名比如 /live/test2那么播放地址也会对应变化。4.2 常用推流源场景除了用视频文件FFmpeg 还能直接采集系统设备推流。想推摄像头画面可以用类似下面这种命令ffmpeg -f v4l2 -i /dev/video0 \ -c:v libx264 -preset veryfast -tune zerolatency \ -f flv rtmp://127.0.0.1:1935/live/cameraWindows 上推摄像头设备名写法不一样一般会用 dshow。macOS 上可以用 avfoundation。不同系统的设备标识不同但核心还是那套编码参数换汤不换药。想推桌面在 Linux 上可以用 x11grabffmpeg -f x11grab -video_size 1280x720 -framerate 15 -i :0.0 \ -c:v libx264 -preset veryfast -tune zerolatency \ -f flv rtmp://127.0.0.1:1935/live/desktop这些采集命令在不同系统下差异较大如果你是跨平台开发建议先拿文件推流跑通全链路再研究设备采集别一开始就让摄像头盖过优先级。4.3 推流状态确认推流命令运行后终端会滚动输出编码日志看到类似 frame xxx fps xx q xx 之类的信息说明 FFmpeg 正在正常工作。这时可以去 node-media-server 的控制台看会输出一条新流接入的日志信息包括推流地址、流ID、状态等。确认流已经接入后再用 VLC 打开网络流地址写http://127.0.0.1:8100/live/stream.flv如果 VLC 能播放出来说明服务端的分发链路没问题。这时候再去做前端心理就踏实多了。5. 前端播放flv.js 接入5.1 播放器页面完整代码现在到了最后一步写一个 HTML 页面用 flv.js 播放 HTTP-FLV 流。我在项目里创建了 index.html代码如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleHTTP-FLV 直播测试/title style body { background: #1e1e1e; color: #eee; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; display: flex; min-height: 100vh; align-items: center; justify-content: center; } .player-box { background: #111; padding: 20px; border-radius: 12px; } video { width: 720px; max-width: 100%; background: #000; border-radius: 8px; } .status { margin-top: 12px; font-size: 14px; color: #aaa; } /style /head body div classplayer-box video idvideoElement controls muted autoplay/video div classstatus idstatusText正在连接.../div /div script srchttps://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js/script script if (flvjs.isSupported()) { const videoElement document.getElementById(videoElement); const statusText document.getElementById(statusText); const player flvjs.createPlayer({ type: flv, isLive: true, url: http://127.0.0.1:8100/live/stream.flv, enableStashBuffer: false, stashInitialSize: 128 }); player.attachMediaElement(videoElement); player.load(); player.play().then(function () { statusText.textContent 播放状态正常; }).catch(function (error) { statusText.textContent 播放失败 error.message; }); player.on(flvjs.Events.ERROR, function (errorType, errorDetail) { console.error(flv.js error:, errorType, errorDetail); statusText.textContent 播放出错 errorType errorDetail; }); } else { document.getElementById(statusText).textContent 当前浏览器不支持 flv.js; } /script /body /html这段代码的核心思路是先判断浏览器是否支持 flv.js如果支持就创建播放器实例传入 isLive: true让 flv.js 以直播模式工作然后把它绑定到 video 元素上load 之后立即 play。5.2 播放器状态管理在播放器状态这里我建议把错误事件和播放状态分开处理不要只靠 play 的 Promise。实际直播过程中推流端可能中断、网络可能抖动flv.js 会触发 ERROR 事件。在这些事件里输出日志、更新页面提示能让你第一时间发现问题。另外要给 video 元素加上 muted autoplay 属性浏览器自动播放策略普遍要求静音下才能自动起播。如果你期望用户看到画面后再点击开启声音那 muted 就保留着好了。如果直播就是要立刻出声可以先 muted 播放再在用户点击按钮时去掉静音这是兼容自动播放策略常用的做法。5.3 延迟与缓冲的权衡flv.js 有两个参数很有意思一个是 enableStashBuffer一个是 stashInitialSize。enableStashBuffer 默认是 true它会启动一个内部缓冲池来吸收网络抖动视频会更流畅但代价是延迟升高。直播场景我一般把它设为 false让数据尽量直接进入播放器延迟可以降下来。stashInitialSize 是缓冲池的初始大小单位是 KB。设得太大会导致播放启动后很长时间才开始出画面太小则可能在网络抖动时频繁卡顿。比较保守的做法是 128 或者 256先保证起播后续再根据实际网络微调。这里想明确一点延迟和卡顿永远是一对矛盾。同一路网络下你要低延迟就不能抱怨偶尔跳帧你要顺滑就不能嫌弃延迟大。自己的直播场景到底更在乎哪一头需要提前想清楚。6. 常见问题与排查实录6.1 flv.js 黑屏不显示这是被问得最多的一个问题。黑屏通常先确认几个点。第一先用 VLC 播一下 HTTP-FLV 地址如果 VLC 也黑屏那很可能是服务端或推流端有问题。看看 FFmpeg 日志是不是在持续输出node-media-server 有没有报错。如果这两端都正常就排查 node-media-server 的 gop_cache关掉后再重推流试试有些播放端拿不到关键帧就会一直黑屏。第二浏览器控制台有没有报错。常见报错包括跨域、404、流格式不支持。如果控制台出现 404检查播放地址里的流名是否和推流流名一致多一个斜杠或者大小写不对都会导致这个问题。第三检查推流端的视频编码。flv.js 对 H.264 支持最好如果 FFmpeg 意外把视频编成了 MPEG4 或者其他编码播放器就算能拿到数据也无法正常显示。推流时明确用 libx264 就能规避。6.2 延迟持续增大直播过程中延迟从 2 秒慢慢涨到 10 秒甚至更高大概率是缓冲策略的问题。首先把 flv.js 的 enableStashBuffer 改为 false很多场景下能立刻改善。如果还在增长看看 FFmpeg 推流端是不是设置了过大的 GOP 大小关键帧间隔越长播放器为了拼接画面越要缓冲更多数据。另外一个隐藏的因素是 node-media-server 里的 gop_cache。它给新进播放端缓存最近的关键帧让新播放端快速出画但也使得播放的起点总会落后一段时间。如果你的场景追求低延迟可以把它关掉让播放端直接追当前实时流。6.3 服务端口占用启动 node-media-server 时如果提示 EADDRINUSE说明 1935 或 8100 端口已经被其他程序占用。可以用下面命令查是谁占用了端口lsof -i :1935找到进程号后按情况决定是否结束它或者直接改 server.js 里的端口号。记得改完端口后前端播放地址也要跟着改不然会出现服务端在 8101 监听前端还在请求 8100 的尴尬情况。6.4 跨域导致无法播放浏览器里 flv.js 跨域拉流需要服务端设置响应头 Access-Control-Allow-Origin。node-media-server 里 http.allow_origin 设为 * 就是允许所有来源但某些老版本对 OPTIONS 预请求支持不到位也可能导致跨域失败。稳妥的做法是在前端页面同源环境下测试也就是说播放页面也通过同一个端口访问比如放在 node-media-server 的某个静态目录。或者你在自己的 Nginx 里给 HTTP-FLV 请求加跨域头。具体配置在下面的优化部分会展示。7. 上线前可做的几点优化7.1 增加鉴权如果你只是局域网自己人看裸奔勉强能接受。一旦开放到公网或跨部门就需要鉴权了。最简单的做法是给播放地址加一个 token 参数在 node-media-server 的鉴权回调里做校验。node-media-server 支持通过 auth 配置接入一个鉴权模块但这样要写额外插件略复杂。如果你用 Nginx 做分发可以用更简单的 HTTP 层鉴权。我这里推荐保守方案先把播放端口限制在内网或加白名单再在页面登录后才加载播放器这样至少能挡住绝大多数乱拨端口的访问。7.2 Nginx 统一分发在单机演示中node-media-server 的 HTTP 端口直接暴露没问题。如果直播环境和已有业务系统融合我更推荐用 Nginx 做反向代理和统一入口。这样可以把 1935 推流端口暴露到内网但把播放入口统一收敛到 80 或 443。Nginx 里做一个简单的流媒体代理配置把 /live/ 前缀下的请求转发到 node-media-server 的 8100 端口同时加上跨域头location /live/ { proxy_pass http://127.0.0.1:8100/live/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers *; }这样浏览器播放地址就可以写成 http://你的域名/live/stream.flv看起来正规很多也方便部署 HTTPS。如果你有 HTTPS 证书把监听端口改成 443播放地址就能全部走加密连接避免内容被中间窥探。7.3 监控与日志直播系统最怕的是无声无息挂掉。node-media-server 本身有控制台日志但生产环境还是建议把日志导入文件或者统一日志平台。可以简单地把进程交给 PM2 管理npm install -g pm2 pm2 start server.js --name live-service pm2 logs live-service pm2 monitPM2 能自动重启崩溃的进程还能看 CPU 和内存对直播这种长连接服务是很实用的。我常驻的几台测试机都是这样跑着的能省不少人工盯控的精力。7.4 更复杂的扩展思路这套链路还可以继续往上加功能。比如想实现多路流管理可以在应用名里做区分/live/camera1、/live/camera2。想录制直播内容可以在 FFmpeg 推流的同时用 tee muxer 输出一份 FLV 文件到磁盘实现随推随录。想兼容手机端可以再开 HLS 输出虽然延迟高一些但 iOS 的 Safari 也能直接看。如果你真的想往音视频底层走还可以研究 node-media-server 的插件机制自己写一个转发脚本把一路 RTMP 流转推到多个平台做多平台分发。这些都建立在这套最小闭环跑通的基础上先把基础链路搞明白了后面扩展就是水到渠成的事。最后分享一个真实经验任何直播系统推流端、服务端、播放端三端的时钟和状态都要能随时摸清。我每次调试时都会把 FFmpeg 日志、node-media-server 控制台、浏览器控制台全打开把三端时间对上。很多看起来诡异的问题比如视频定时卡顿、延迟逐渐变大其实只要把三端日志一对比原因通常立刻浮出水面。你在做这个项目的时候也养成这个习惯后面排查问题的速度会快很多。