做 MiBee NVR 的 ONVIF 协议支持时,我需要深入理解 ONVIF 的完整流程——从 WS-Discovery 设备发现到流地址获取,光看文档和抓包不够,得有一个能跑的 ONVIF 相机服务端来对接调试。手边刚好有台树莓派 3B + OV5647 摄像头,本来跑着 MediaMTX 推 RTSP 流,但 MediaMTX 没有 ONVIF 服务端模式(GitHub issue #1402),NVR 发现不了它。
于是决定自己写一个轻量级的 Go ONVIF 相机服务,把树莓派上原来的 MediaMTX 替换掉。这样做的好处是 MiBee NVR 的 ONVIF 客户端和这个相机服务端用的是同一个 0x524a/onvif-go 库,客户端和服务端完全兼容,调试起来特别方便。
为什么要造 rpi-cam
起因很简单:开发 MiBee NVR 的 ONVIF 协议支持,需要一个本地的 ONVIF 相机来调试。过程中遇到的痛点大概有这么几个:
MediaMTX 缺 ONVIF 服务端。MediaMTX 确实是个好工具,RTSP 流媒体做得不错,但问题在于它没有 ONVIF 服务端模式(GitHub issue #1402)。你用它推流可以,但 NVR 无法通过 ONVIF 协议发现和控制这个相机。
现有 ONVIF 方案太重。找了一圈,要么是 Python 写的性能不行,内存占用高;要么是功能不完整,只支持 Device 服务,缺 Media/PTZ/Imaging 服务;要么是需要 CGO 编译,交叉编译太麻烦。
相机控制不完整。很多开源方案只推个流,相机的亮度、对比度、白平衡这些参数根本控制不了。但实际项目中,NVR 往往需要根据场景调整相机参数。
资源占用太高。树莓派 3B 只有 905MB 内存,之前 MediaMTX + mtxrpicam 就占了 45MB,跑起来偶尔还会因为内存压力重启。换成 rpi-cam 之后实测只占 15-25MB,而且跑了几个月没掉过线,比 MediaMTX 稳定不少。
相机支持单一。现在手里可能有 OV5647、IMX219、IMX708,将来还要换 IMX477,甚至 USB 摄像头。方案得支持多种相机,不能换了相机就得重写代码。
我的需求其实很简单:
- 一个二进制文件,下载就能跑
- 支持 ONVIF Profile S,和 MiBee NVR 兼容
- 内存占用 <30MB,适合 905MB 内存的小设备
- 支持 CSI 和 USB 两种相机接口
- RTSP 推流 + RTMP 推送到云服务
- 相机参数实时控制
没有现成的,那就自己造。
rpi-cam 是什么
rpi-cam 是一个用 Go 写的轻量级树莓派 ONVIF 相机服务,专门为资源受限的 ARM 设备设计。它提供完整的 ONVIF Device/Media/PTZ/Imaging 服务,支持 RTSP 流媒体、RTMP 推流和 WS-Discovery 自动发现。
整体架构
rpi-cam 围绕几个核心需求设计,架构简洁高效:
flowchart TB
subgraph 相机层
CAM["CSI/USB 相机"]
end
subgraph rpi-cam
CAP["相机捕获"]
RTSP["RTSP 服务器"]
ONVIF["ONVIF 服务"]
RTMP["RTMP 推流"]
CTRL["相机控制"]
end
subgraph 外部系统
NVR["NVR/VMS 系统"]
CLOUD["云服务"]
end
CAM --> CAP
CAP --> RTSP
CAP --> RTMP
RTSP --> ONVIF
CTRL --> ONVIF
ONVIF --> NVR
RTMP --> CLOUD
classDef cam fill:#E3F2FD,stroke:#1565C0,color:#1565C0
classDef app fill:#FFF3E0,stroke:#E65100,color:#BF360C
classDef ext fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
class CAM cam
class CAP,RTSP,ONVIF,RTMP,CTRL app
class NVR,CLOUD ext
相机捕获通过 libcamera 接口,支持 OV5647、IMX219、IMX708、IMX477 等模块。RTSP 服务器使用和 MediaMTX 同样的 gortsplib 库,确保流媒体兼容性。ONVIF 服务提供完整的设备发现、媒体控制、PTZ 操作和图像参数调节。RTMP 推流支持阿里云、腾讯云等云服务。
内部实现采用标准的 Go 分层架构:
Handler → Service → Repository → 配置文件,配置通过 YAML 管理,易于部署和维护。
核心功能
ONVIF 服务端。完整的 ONVIF Profile S 支持,包括:
- Device Service:设备信息、能力查询、WS-Discovery
- Media Service:媒体配置、流地址获取、快照
- PTZ Service:数字 PTZ 控制(裁剪实现)
- Imaging Service:亮度、对比度、饱和度、锐度调节
和 MiBee NVR 使用同一个 onvif-go 库,客户端和服务端完全兼容,测试起来特别方便。
RTSP 流媒体。使用 MediaMTX 同样的 gortsplib 库,确保和现有 NVR 系统的兼容性。支持 720p@15fps、1080p@30fps 等多种分辨率配置。
RTMP 推流。集成 lal 库,支持推送到阿里云、腾讯云、Twitch、YouTube 等云服务。可以用简单的配置实现云端存储和直播。
相机控制。支持实时的图像参数调节:
- 基础参数:亮度、对比度、饱和度、锐度
- 高级参数:曝光模式、曝光时间、增益、白平衡模式
- 图像处理:水平翻转、垂直翻转
- 数字 PTZ:通过软件裁剪实现平移、缩放
多相机支持。支持常见的树莓派相机模块:
| 模块 | 传感器 | 分辨率 | 对焦 | 特点 |
|---|---|---|---|---|
| Pi Camera V1 | OV5647 | 2592×1944 | 固定 | 基础 5MP,当前使用 |
| Pi Camera V2 | IMX219 | 3280×2464 | 固定 | 低光性能更好 |
| Pi Camera V3 | IMX708 | 4608×2592 | 自动对焦 | PDAF,HDR 支持 |
| Pi HQ Camera | IMX477 | 4056×3040 | 手动对焦 | 可更换镜头 |
| USB UVC | 各种 | 各种 | 各种 | 即插即用 |
实际应用场景
NVR 集成。这是 rpi-cam 的本职工作——作为 MiBee NVR 的测试相机:
sequenceDiagram
participant CAM as rpi-cam
participant NVR as MiBee NVR
participant CLOUD as 云服务
Note over CAM,NVR: ① WS-Discovery 发现
NVR->>CAM: Probe 请求
CAM-->>NVR: ProbeMatch (XAddr)
Note over CAM,NVR: ② 获取设备信息
NVR->>CAM: GetDeviceInformation
CAM-->>NVR: 制造商/型号/固件
Note over CAM,NVR: ③ 获取媒体配置
NVR->>CAM: GetCapabilities + GetProfiles
CAM-->>NVR: 编码器/分辨率
Note over CAM,NVR: ④ 获取流地址
NVR->>CAM: GetStreamUri
CAM-->>NVR: rtsp://host:port/stream
Note over CAM: ⑤ RTSP 播放
NVR->>CAM: SETUP / PLAY
CAM-->>NVR: H.264 视频流
Note over CAM,CLOUD: ⑥ RTMP 推流
CAM->>CLOUD: RTMP 推流
CLOUD-->>CAM: 推流确认
这个流程覆盖了大多数 NVR 系统的集成场景,从设备发现到视频播放,再到云端存储,一气呵成。
宠物孵化箱监控。这是 rpi-cam 的一个意外收获。家里养宠物需要观察孵化箱里的情况,市面上的微距摄像头体积都很大,塞不进箱子里。但树莓派的 CSI 相机模块只有指甲盖大小,用排线引出来,摄像头贴在孵化箱内部,树莓派放在外面,轻松解决空间问题。配合 ONVIF 的 Imaging 服务可以远程调亮度、对比度,不用打开箱子就能看清细节。
同一摄像头的 RTSP vs ONVIF 对比。在 MiBee NVR 监控大屏上,同一个 rpi-cam 摄像头分别通过 RTSP 直连和 ONVIF 发现接入,效果对比:
左是RTSP直连,右是ONVIF发现
技术选型
选技术的时候有几个硬约束:
- 纯 Go:不依赖 CGO,交叉编译方便,树莓派 3B 上能跑
- 内存占用小:目标 <30MB 内存,不能像 MediaMTX 那样占 45MB
- 稳定可靠:相机不能掉线,NVR 接连不上就麻烦了
- 兼容性好:和现有 NVR 系统无缝对接
最终的技术栈:
| 组件 | 选择 | 理由 |
|---|---|---|
| ONVIF 服务端 | 0x524a/onvif-go | 纯 Go,完整的 Device/Media/PTZ/Imaging 服务 |
| RTSP 服务器 | bluenviron/gortsplib/v5 | MediaMTX 同款库,兼容性有保证 |
| RTMP 推流 | q191201771/lal | Go 原生,资源占用适中,维护活跃 |
| 相机捕获 | MediaMTX rpicam | 经过验证的 libcamera 接口,不需要 CGO |
| 配置管理 | YAML | 人类可读,易于部署 |
核心亮点是零 CGO。本来考虑过 go4vl 直接操作 V4L2,但需要 CGO 交叉编译,太麻烦。最后选择 MediaMTX 的 rpicam 方式,用 subprocess 调用,Go 层面处理 pipe 流,编译部署简单多了。
整个项目只有不到 20 个 Go 文件,依赖也很少。go mod tidy 之后也就几个核心库,维护起来很轻松。
实际部署
树莓派 3B,905MB 内存,WiFi 连网。之前跑 MediaMTX 大概占 45MB 内存,偶尔会因内存压力自动重启。换成 rpi-cam 之后稳定在 15-25MB,跑了几个个月没出过问题。
编译部署
# 克隆项目
git clone https://github.com/Mi-Bee-Studio/raspberrypi-camera
cd raspberrypi-camera
# 交叉编译到 arm64
GOOS=linux GOARCH=arm64 go build -o build/rpi-cam ./cmd/...
# 复制到目标设备
# 复制到目标设备
scp build/rpi-cam user@your-rpi-host:~/rpi-cam
### 配置文件
`config.yaml` 配置示例:
```yaml
# 相机配置
camera:
width: 1280
height: 720
fps: 15
bitrate: 2000000 # 2Mbps
# RTSP 配置
rtsp:
port: 8554
# ONVIF 配置
onvif:
port: 8080
username: admin
password: your-password
# RTMP 推流配置
rtmp:
enabled: true
targets:
- url: "rtmp://cloud.example.com/live/stream"
key: "your-stream-key"
systemd 服务
/etc/systemd/system/rpi-cam.service:
[Unit]
Description=rpi-cam ONVIF Camera Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=rpi-cam
ExecStart=/opt/rpi-cam/rpi-cam -config /opt/rpi-cam/config.yaml
WorkingDirectory=/opt/rpi-cam
# Security hardening
ReadWritePaths=/opt/rpi-cam
[Install]
WantedBy=multi-user.target
启用服务:
sudo systemctl daemon-reload
sudo systemctl enable --now rpi-cam
部署完成后,NVR 就能自动发现这个相机了。WS-Discovery 会广播到局域网,NVR 收到 Probe 请求后会返回完整的设备信息。
开源地址
rpi-cam 已经开源,感兴趣的可以下载试玩:
- rpi-cam:github.com/Mi-Bee-Stud… (MIT 许可证)
项目文档比较全,架构设计、部署指南、API 接口都有。如果要在生产环境使用,建议先在测试环境跑一段时间,确保稳定性。
写在最后
这个项目最初就是为了给 MiBee NVR 开发 ONVIF 协议支持时有个能调试的对端。开发过程中把树莓派上原来的 MediaMTX 替换了,没想到实际跑下来资源占用更低、也更稳定——算是意外之喜。后来又发现拿来监控孵化箱特别好使,CSI 相机的小体积是普通微距摄像头比不了的。
现在 rpi-cam 在我这里同时跑着两个用途:给 NVR 做测试相机,以及 7×24 监控孵化箱。一个二进制文件,一个配置文件,systemd 托管,基本不用管。
当然,项目还有很多可以改进的地方:
- 数字 PTZ 的实现可以更完善
- 支持更多的相机型号
- 添加 Web 管理界面
- 性能优化和监控
如果你也需要一个轻量级的树莓派 ONVIF 相机服务,或者刚好需要把小体积摄像头塞进什么狭小空间里,不妨试试看。个人精力有限,前期主要围绕自己的需求迭代,有想法或者发现了 bug 欢迎提 issue 或 PR。