一个跑在树莓派上的开源 NVR,凭什么能跟 OBS 抢活干?这篇讲讲我们怎么把"监控摄像头 → 直播间"这条路从能用走到好用,中间踩了多少协议层的坑。

图片

上图:手机 App 上看到的直播画面,推流由 MiBeeNVR 原生 Go relay 完成,无 FFmpeg 依赖。


先说结论

MiBeeNVR v0.8.1 支持将任意接入的摄像头画面,通过 RTMP 协议直接推流到直播平台。 纯 Go 实现,不需要额外安装 FFmpeg,不依赖外部进程,配置好推流地址就能开播。

⚠️ 目前作者仅在斗鱼平台完成实测,其它平台理论上兼容(同为 FMS 标准),但尚未验证。遇到问题请到 GitHub Issues 反馈。

树莓派 3B 以上即可运行,支持 H.264/H.265 摄像头。


MiBeeNVR 是什么

MiBeeNVR 是一个开源的网络视频录像机(NVR)项目,用 Go 语言编写,专为嵌入式设备(树莓派、香蕉派等)优化。在 v0.8.1 之前,它已经具备:

  • 多路摄像头管理与录像(RTSP/ONVIF/小米双摄)

  • H.264/H.265 硬件转码

  • 延时摄影录制与合成

  • LL-HLS / WebRTC 低延迟播放

  • Docker 一键部署

唯独缺一样东西:直播推流。

社区里一直有人问:"能不能把摄像头画面直接推到直播间?"——养宠物的想开宠物直播间,做安防的想做个公开监控频道,玩硬件的就想折腾。v0.8.1 把这个能力补上了。


直播推流:看着简单,做起来要命

第一步:先跑通

最直观的方案是调 FFmpeg——它是直播行业的瑞士军刀,OBS 底层也用它。在 MiBeeNVR 的"转推输出"里配一个 RTMP 目标,后端起一个 FFmpeg 子进程,画面确实能推到直播间。

[Image: MiBeeNVR 转推输出配置界面截图]

但问题是:FFmpeg 是外部进程。要单独安装、单独管理生命周期、占用额外内存。在一个目标是"树莓派上轻量运行"的项目里,拖着一个几十 MB 的外部进程跑推流,不够优雅。

我们想要的是:纯 Go 实现的 RTMP 推流,嵌进 NVR 进程里,零外部依赖。

第二步:纯 Go 推流的六层地狱

RTMP 协议看着简单——不就是握手、连接、推数据嘛。但真要对接国内直播平台(它们的后端都是 Adobe FMS 兼容实现),会发现每一层都能卡住你。

我们遇到的不是一个问题,是六层叠加的问题:

L1握手digestHMAC-SHA256签名L2chunksize分块大小不匹配L3chunkheaderType0/1/2/3格式L4后台读消息缓冲区撑爆L5元数据字段缺width/height/fpsL6streamID字节序大端vs小端⭐✅推流成功

前四层修完,握手通过了、连接建立了、推流命令成功了——但第一个视频帧发出去 74 毫秒,连接就被对方重置了。

这个 74ms 非常关键:它说明不是网络超时(那种要等 5 秒),而是对方在帧级别就拒绝了你的数据。

最终 Boss:一个字节序的故事

最后的根因藏得极深,以至于我们花了整整一天才找到。

RTMP 消息里有个字段叫 MessageStreamID,用来标识消息属于哪条流。按 RTMP 规范,这个字段应该是小端序(little-endian)。我们代码里就老老实实按规范写了小端序。

但所有主流实现(FFmpeg、OBS、gortmplib)都用了大端序。

直播平台也期望大端序。

我们的代码在大部分消息上走的是第三方库的标准路径(大端序),但在某些自定义写入路径上用了我们自己的实现(小端序)。同一个连接里,一会儿大端一会儿小端——对方收到这种精神分裂的数据包,直接 RST 断开。

发现这个 bug 靠的是进程内字节级 dump——把每个发出去的字节都记下来,逐条解析 chunk header,才发现 streamID 字段的大小端不一致。修复只改了 4 行代码,但找到它花了一整天。

Go标准WriterstreamID=大端直播平台期望大端✅我们的WriterstreamID=小端直播平台收到小端❌RST

一句话总结:规范说往左走,全世界都往右走,你跟着规范走,就被全世界拒绝。


最终效果

六层全部修复后:

  • ✅ 纯 Go relay 推流成功,无 FFmpeg 依赖

  • ✅ TCP 连接稳定保持,持续推流无中断

  • ✅ 树莓派摄像头画面实时推到直播间,延迟约 2-3 秒

  • ✅ 支持 H.264 摄像头,后续将支持 H.265 推流

⚠️ 不建议开启 use_ffmpeg 选项。 FFmpeg 推流存在已知问题:播放中途可能因帧率不匹配导致卡顿,且不会自动恢复。纯 Go relay 路径已经过充分验证,是推荐的推流方式。

💡 纯 Go relay 是本次开发的重点。如果在其它平台遇到 Go 推流的问题,请优先提 Issue 反馈 Go 方面的问题,帮助项目持续改进推流能力——而不是切回 FFmpeg。


怎么用

方式一:MiBeeNVR 界面配置

在摄像头设置里找到"转推输出",填入直播平台的 RTMP 推流地址即可:


rtmp://<推流地址>/live/<推流码>

推流码从直播平台的推流设置里获取。

方式二:Docker 部署


docker run -d \
  --name mibee-nvr \
  -p 8080:8080 \
  -v mibee-nvr-data:/var/lib/mibee-nvr \
  ghcr.io/mi-bee-studio/mibee-nvr:latest

打开 http://<设备IP>:8080 进入管理界面,添加摄像头后配置转推输出。

测试情况与注意事项

已测试平台:斗鱼直播(直播伴侣模式,局域网推流)✅

理论兼容:所有 FMS 标准的直播平台后端。但各平台实现细节有差异,未经实测的平台可能遇到问题——遇到问题请到 GitHub Issues 反馈,作者会跟进。

⚠️ 重要提醒

不建议长期在国内直播平台持续推流。 各平台有自己的推流政策和限制,长时间无人值守的摄像头推流可能触发:

  • 限流(降低码率/分辨率)

  • 推流码过期后断连

  • 账号警告甚至封禁

请务必了解你所用平台的相关政策,遵守平台规则。MiBeeNVR 提供的是技术能力,怎么用、用在哪个平台、推什么内容,责任在用户自己。


踩坑感悟

这次做直播推流,最大的感悟有三条:

1. 对比实验是调试的黄金标准

当 Go 代码推流失败时,我们不确定是代码问题还是环境问题。直到在同一个设备上用 FFmpeg 命令行手动推流成功,才确认是 Go 协议层的锅。没有这个对照,再多猜测都是空谈。

2. 二进制协议的 bug,必须看字节

字节序这种问题,靠读代码是看不出来的——因为代码"看起来是对的"(按规范写的)。只有把实际发出的字节 dump 出来逐条解析,才能发现大小端不一致。调试二进制协议,先加 dump,再分析。

3. 规范不等于现实

RTMP 规范说 MessageStreamID 是小端,但全世界都用大端。这种"规范与实践脱节"的情况在旧协议里非常常见。当你按规范写代码却到处碰壁时,去看看别人的实现是怎么做的——事实标准比纸面规范更有说服力。


关于 MiBeeNVR

MiBeeNVR 是 Mi-Bee-Studio 的开源项目,使用 Go 语言编写,已为树莓派等嵌入式设备优化。

GitHub: https://github.com/Mi-Bee-Studio/MiBeeNvr

主要特性:

  • 多路摄像头管理(RTSP / ONVIF / 小米双摄)

  • H.264/H.265 硬件转码(V4L2 / VAAPI / NVENC 自动探测)

  • 延时摄影录制与合成(纯 Go muxer,零编码开销)

  • LL-HLS / WebRTC 低延迟播放

  • RTMP 直播推流(v0.8.1 新增,纯 Go 实现)

  • Docker 一键部署

运行环境:树莓派 3B+ 及以上 / x86_64 / ARM64 / Docker


MiBeeNVR v0.8.1 已发布,完整变更列表见 GitHub Release Notes。

如果你也在用树莓派做有意思的项目,欢迎来 GitHub 交流。

Logo

邀请您加入社区

更多推荐