MiBeeNvr v0.7.0: 延时摄影 v2 + 小米双摄 + H.265 封装 + 发布加固

v0.6.0 把延时摄影管道搭起来之后,社区反馈很快就指出了几个硬伤:JPEG 序列要吃掉太多存储、H.265 摄像头生成的延时片段播放不了、双摄小米设备只能抓到主镜头、偶尔还会出现 H.265 HLS 直接 panic 的崩溃。这些都不是边缘场景——双摄 CW500 和室外摄像头 4 在国内出货量很大,H.265 已经是中高端摄像头的事实标准。v0.7.0 的主线就是围绕这些反馈展开的。

这一版把延时摄影管道整体重写成了 v2:用纯 Go 实现的 H.264/H.265 NAL→MP4 muxer 取代了之前依赖 FFmpeg 的合并路径,关键帧文件自包含 SPS/PPS,合并出来的就是可直接播放的 H.264/H.265 视频,不再生成中间 JPEG 序列。小米双摄支持通过通道选择接入,CW500、室外摄像头 4 这类设备能指定镜头;H.265 HLS 的 panic 被 StreamHub 重构和 muxer 自动恢复彻底治住了。转码模块也借这版做了一次分层重构——硬件探测、编码器抽象、任务管道拆成独立层,新增编码器和硬件后端不再牵动整个管道。前端 Dashboard 则做了轮大整理:AI 模块移除、摄像头管理简化、延时和普通录像合并到统一列表。

发布前在真实摄像头环境下充分验证了这些改动,包括 CW500 双摄、H.265 摄像头的 HLS/延时录制、以及 Docker 部署的存储路径与 GPU 直通场景。三个自研摄像头项目也跟着同步更新——一个改了名(MiBeeHomeCam → MiBee Cam,新增 ONVIF 自动发现)、一个换了仓库和定位(rpi-cam → mibee-eye-raspi,覆盖所有 libcamera 兼容主板)、一个修了上版的遗留 bug,文末单开一节讲。更早一轮(v0.6.0 同期)的三个项目细节见 camera-test-machines。完整变更列表见 GitHub Release Notes

延时摄影 v2 管道

v2 的核心变化是合并路径不再经过 FFmpeg,关键帧直接以 H.264/H.265 NAL 单元的形式进入自研 muxer,封装成自包含的 MP4:

mermaid
flowchart TB
    SRC["RTSP / 小米源<br/>h264 / h265"] --> KE["关键帧抽取器<br/>SPS/PPS 缓存"]
    KE --> KF["H.264 / H.265<br/>关键帧序列"]
    KF --> RM["滚动合并<br/>纯 Go muxer"]
    RM --> OUT["自包含 MP4<br/>可直接播放"]

    classDef source fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    classDef rec fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef store fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    classDef out fill:#F3E5F5,stroke:#9C27B0,color:#6A1B9A
    class SRC source
    class KE,RM rec
    class KF store
    class OUT out

v0.6.0 的管道在 JPEG 序列上做合并,合并阶段必然要重新编码,CPU 开销和文件体积都下不来。v2 的关键帧抽取器在第一帧就把 SPS/PPS 缓存下来,之后每个关键帧的 NAL 单元都带着这份参数集一起落盘,合并时只需要把 NAL 单元按序封装进 MP4 容器——纯 mux、零编码。

双模式关键帧抽取

v2 抽取器支持两种工作模式,由每个摄像头独立配置:

1
2
interval 模式:  按 config.timelapse.interval 抽帧 (默认 30s)
IDR 模式:       直接复用 RTSP 流中的 IDR 帧,不做时间间隔过滤

interval 模式适合 MJPEG 源或需要降低帧率的高频 RTSP 流;IDR 模式适合 H.264/H.265 流——摄像头本身的 GOP 决定了关键帧间隔,直接复用 IDR 帧可以完全跳过解码,抽取器只做 NAL 单元过滤和 SPS/PPS 缓存,开销几乎为零。

滚动合并的合并时长

合并不再是固定的「每日一次」,而是提供了 8h / 12h / 24h / 自然天 / 7d / 30d 几档可选。滚动合并管理器按合并时长切分时间窗口,窗口结束后触发合并任务;用户也可以在前端通过 retry-merge API 手动触发合并。

延时录像现在和普通录像一起出现在统一的录像列表里,前端不再需要单独的延时入口。每个摄像头的延时配置(间隔、帧源、合并模式)都可以在摄像头配置编辑器里单独设置。

纯 Go muxer vs FFmpeg 合并

对比维度v2 纯 Go muxerv0.6 FFmpeg 合并
依赖需要 FFmpeg
合并方式NAL 单元直接封装为 MP4JPEG 重新编码为 H.264
编码开销零(仅 mux)高(编码)
输入帧源H.264 / H.265 关键帧JPEG 序列
适用场景长期存档、实时预览已废弃

v2 的 muxer 同时支持 H.264 和 H.265——这对 H.265 摄像头是关键改进,v0.6 的 JPEG 路径没法生成 H.265 延时片段。

转码架构优化

v0.6.0 的转码管道是按 Wave 1/2/3 三波快速堆出来的——队列、轮询、回填各自独立,硬件探测和编码器选择还混在任务执行路径里。v0.7.0 把整个转码模块做了一次分层重构,目标是让新增编码器、新增硬件后端不再动到任务管道。

mermaid
flowchart TB
    Q["任务队列<br/>SQLite 持久化"] --> WP["Worker 池<br/>并发消费"]
    WP --> PR["硬件探测层<br/>V4L2M2M / VAAPI / NVENC"]
    PR -->|"设备可用"| HW["硬件编码<br/>vaapi / h264_v4l2m2m / h264_nvenc"]
    PR -->|"设备不可用"| SW["软件回退<br/>libx264 / libx265"]
    HW --> EX["编码器抽象层<br/>统一命令构造"]
    SW --> EX
    EX --> FF["FFmpeg 子进程<br/>进度回调 + 超时控制"]

    classDef q fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    classDef w fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef p fill:#F3E5F5,stroke:#9C27B0,color:#6A1B9A
    classDef hw fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    classDef ex fill:#FFFDE7,stroke:#F9A825,color:#BF6F00
    classDef ff fill:#FFEBEE,stroke:#C62828,color:#C62828
    class Q q
    class WP w
    class PR p
    class HW,SW hw
    class EX ex
    class FF ff

分层重构

重构把转码拆成了三层,各司其职:

  • 任务管道层:SQLite 持久化的任务队列 + Worker 池。Worker 池按并发度消费任务,每个任务的生命周期(pending → running → done/failed)由队列管理,和具体编码器无关。
  • 硬件探测层:在转码引擎初始化阶段一次性完成,结果缓存。VAAPI 探测会检查 /dev/dri/renderD* 是否存在;V4L2M2M 探测会枚举 /dev/video* 的编码节点;NVENC 探测走 FFmpeg 的 -encoders 输出。探测不到的硬件后端直接关闭该能力,运行时不再重复探测。
  • 编码器抽象层:把硬件编码器、软件编码器收敛到统一的命令构造接口。新增编码器只需要实现这个接口,任务管道和 Worker 池完全不用动。

分层之前,编码器选择和硬件探测散落在任务执行的多个分支里,加一个新编码器要改四五处。重构后新增 libx265 软编码、新增硬件后端都只动编码器抽象层。

能力加强

在架构基础上,这版补了几个之前缺的能力:

  • libx265 软编码:v0.6.0 的软件回退只有 libx264,H.265 摄像头转码时无硬件编码器就只能转成 H.264。补上 libx265 后,H.265→H.265 的纯软件转码路径打通,ARM 平台(树莓派、香蕉派)在没有硬件编码器时也能保持编码格式不变。
  • 转码与延时摄影共享探测/编码器:v2 延时管道的 H.265 muxer 复用了转码模块的 SPS/PPS 解析和硬件探测结果——延时录像需要重新编码时(比如 JPEG 序列转 H.264 用于长期存档),直接走转码的编码器抽象层,不再各自维护一套 FFmpeg 调用。
  • MJPEG 输入的软件编码:之前 MJPEG 输入源在 ARM 上被错误地禁用了软件编码,修复后 MJPEG 输入也能走 libx264/libx265。

可靠性

任务管道的可靠性也做了加强:失败任务的重试不再丢上下文(Worker 重启后从 SQLite 恢复队列状态),FFmpeg 子进程的超时和进度回调独立监控,避免单个转码任务卡死拖垮整个 Worker 池。加上前面提到的 VAAPI 设备校验和 FFmpeg 状态测试隔离,转码在 Docker、虚拟机、ARM 板子上的稳定性都明显改善。

小米双摄支持

双摄小米设备(CW500、室外摄像头 4 等)的两组镜头共用一个设备 ID,go2rtc 这类上游项目在处理时存在通道限制。v0.7.0 在小米云集成里加了通道选择,用户在添加摄像头时可以指定镜头。

mermaid
sequenceDiagram
    participant U as 用户
    participant NVR as MiBeeNvr
    participant XC as 小米云
    participant CAM as 双摄设备

    U->>NVR: 添加摄像头 + 选择镜头
    NVR->>XC: 解析设备流地址 (did + channel)
    XC-->>NVR: 返回指定通道的 MISS URL
    NVR->>CAM: miss connect (指定通道)
    CAM-->>NVR: 该镜头的 RTP 流
    Note over NVR: 单独的 recorder 实例<br/>独立录制与转码

通道选择作用于 MISS URL 解析阶段——小米云返回的是设备级地址,MiBeeNvr 在建立 recorder 时把 channel 参数带上,拿到的就是指定镜头的流。每个镜头对应一个独立的 recorder 实例,录制、转码、延时管道都各自独立,和单摄摄像头在数据流上没有区别。

另外修了 test-connection 里的 RTSP URL 凭证处理:之前带特殊字符的密码会把 URL 拼错,导致连接测试一直失败,实际录制却正常——这种「测试失败但能跑」的状态最容易让人困惑。

H.265 HLS 封装改进

#20 报告的 H.265 HLS panic 是这一版的重点修复。崩溃发生在 muxer 还没初始化时,HLS 写入路径直接走到了未初始化的 muxer 指针上:

mermaid
flowchart LR
    IN["RTP 帧"] --> SH{StreamHub<br/>路由}
    SH -->|已初始化| M1["Muxer 正常写入"]
    SH -->|未初始化| RC["自动恢复<br/>重新初始化 Muxer"]
    RC --> M2["恢复后继续写入"]

    classDef in fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    classDef hub fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    classDef rec fill:#FFEBEE,stroke:#C62828,color:#C62828
    class IN in
    class SH hub
    class M1,M2 ok
    class RC rec

根因是 StreamHub 在多路径分发时没有统一处理 muxer 的生命周期——H.265 流的首帧到达时 muxer 可能还没建好,但写入路径没有这层防护。重构后的 StreamHub 集中管理 muxer 状态:写入前检查 muxer 是否就绪,未就绪则触发自动恢复流程,重新初始化 muxer 后再继续写入,而不是直接 panic。

#18 报告的小米摄像头反复重连也是同一类问题在连接层的体现。小米 CS2 设备的 UDP miss connect 在网络抖动时会超时,之前的退避策略固定 1 分钟,且 PONG 响应处理不当——PONG 丢失后连接状态机卡住,导致看起来像是设备掉线、反复触发重连。修复包括:PONG 响应的健壮性处理、退避时间加随机抖动、CS2 缓冲区在重连时主动丢弃残留数据,避免旧数据污染新连接。

发布加固

这一版在稳定性、CI 和打包上做了不少加固工作,主要受 #15 的 Docker 部署反馈驱动。转码模块的 VAAPI 设备校验和 FFmpeg 状态测试隔离归在前面的转码架构优化里讲,这里说其余几项。

WebSocket 解码器背压:多路同时播放时,Worker 到 ConnectionManager 的 WebSocket 解码器没有背压控制,背压上来直接丢帧或卡死。修复把背压信号从 Worker 一路接到 ConnectionManager,多路播放稳定性明显改善。

Docker 存储路径:#15 反馈的容器重启报 mkdir /var/lib/mibee-nvr: permission denied,根因是 entrypoint 里的默认存储路径和实际数据卷路径不一致。修复对齐了路径,重启不再丢权限。

Dashboard UX 大改

v0.6.0 到 v0.7.0 之间,前端 Dashboard 做了一轮比较大的整理。驱动力有两个:一是 v2 延时管道产出的录像需要和普通录像统一展示,旧的「录像」和「延时」分两个入口已经讲不清;二是浏览器端的 AI 检测模块一直没做完,留在界面上反而误导用户。

mermaid
flowchart LR
    subgraph v0.6 ["v0.6.0"]
        A1["摄像头管理<br/>含 Enabled 开关"]
        A2["录像列表"]
        A3["延时入口(独立)"]
        A4["AI 检测(半成品)"]
    end
    subgraph v0.7 ["v0.7.0"]
        B1["摄像头管理<br/>简化,移除 Enabled"]
        B2["统一录像列表<br/>普通 + 延时"]
        B3["转码历史页"]
        B4["AI 模块移除"]
    end
    A1 --> B1
    A2 --> B2
    A3 --> B2
    A4 --> B4

    classDef old fill:#FFEBEE,stroke:#C62828,color:#C62828
    classDef new fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    class A1,A2,A3,A4 old
    class B1,B2,B3,B4 new
  • 移除 AI 模块:浏览器端的目标检测只做了识别、没做跟踪,实际用处不大还占资源。v0.7.0 直接从前端移除,后端 internal/ai/ 的 ONNX Runtime 推理框架保留,作为未来独立功能(实时目标检测)的基础,不再在 Dashboard 里暴露半成品入口。
  • 摄像头管理重构:移除了 Enabled 字段。之前每个摄像头有个「启用/禁用」开关,但这个状态和录制、流媒体、转码的状态机交织在一起——禁用的摄像头偶尔还会因为事件总线触发录制,行为不一致。重构后摄像头的启停统一走录制器生命周期,配置里不再有这个容易踩坑的开关。
  • 统一录像列表:延时录像和普通录像合并到同一个列表,前端按类型筛选。之前延时录像要进单独的页面才能看,v2 管道产出 H.264/H.265 MP4 后,延时片段和普通录像在播放体验上没有差别,没必要分两个入口。
  • 转码历史页:转码任务的状态(pending/running/done/failed)、重试按钮、历史记录分页清理都收进了一个独立页面,之前是散在录像详情里的。

社区反馈与已关闭 Issue

这一版关闭了 4 个社区 issue,都是在真实使用中暴露出来的:

  • #14 多镜头设备支持与视频画质优化 — 小米 CW500 双摄和室外摄像头 4 双摄只能抓到主镜头,且录制画质偏糊。v0.7.0 通过通道选择接入双摄的第二镜头,转码管道支持 H.264/H.265 互转,画质可以通过转码参数调整。
  • #15 5.0 新版本 Docker 测试报告 — Docker 部署的存储路径权限问题、设置页保存入口、H.265 的 HLS/HTTP-FLV/LL-HLS 播放兼容性、以及实时画面时间戳错乱。逐条修复,Docker 路径对齐、设置入口简化、HLS 时间戳回放循环问题定位到 muxer。
  • #18 小米摄像头反复重连 — 小米 CS2 设备 UDP miss connect 频繁超时,重连退避固定 1 分钟,PONG 响应处理不当导致状态机卡住。修复了 PONG 健壮性、退避加抖动、CS2 缓冲区重连清理。
  • #20 H.265 error — H.265 HLS 在 muxer 未初始化时直接 panic。StreamHub 重构统一管理 muxer 生命周期,写入前检查就绪状态,未就绪触发自动恢复。

Bug 修复

  • 转码 VAAPI 设备校验 — 硬件探测在选 VAAPI 编码器前检查 /dev/dri/renderD*,GPU 不可用时降级 libx264/libx265。
  • FFmpeg 状态测试隔离 — 测试不再因本机装了系统 FFmpeg 而失败,CI 解锁。
  • WebSocket 解码器背压 — 背压信号从 Worker 接到 ConnectionManager,多路播放稳定。
  • 滚动合并兼容所有录制器类型 — H.264、H.265、MJPEG 三种录制器的关键帧都能正确合并。
  • 关键帧抽取器缓存 SPS/PPS — 关键帧文件自包含,不依赖外部参数集。
  • 摄像头 H.265 编码检测 — 运行时录制器类型用于关键帧抽取器,不再用静态配置判断。
  • 录像帧列表接口 — 列表包含 H.264/H.265 延时帧,前端能正确展示。
  • i18n 缺失翻译键 — 补全延时协议、转码历史操作相关文案。

升级

配置向后兼容,直接替换二进制即可:

bash
1
2
3
4
5
# Docker
docker pull ghcr.io/mi-bee-studio/mibeenvr:latest

# 或下载二进制
wget https://github.com/Mi-Bee-Studio/MiBeeNvr/releases/latest/download/mibee-nvr-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')

升级后建议检查 config.example.yaml 中的新配置项(timelapse v2 的合并时长、双摄通道选择、转码硬件探测等),按需补充到自己的配置文件中。小米双摄用户需要在摄像头配置里重新选择镜头。

摄像头项目同步更新

和 v0.6.0 一样,v0.7.0 的测试也依赖三个自研摄像头项目提供真实环境。这一轮它们自己也有不少变化——一个改了名、一个换了仓库和定位、一个修了上版的遗留 bug。

MiBeeHomeCam → MiBee Cam(v0.3.0)

基于 Seeed Xiao ESP32-S3 Sense 的固件改了产品名:MiBeeHomeCamMiBee Cam,固件、文档、配置里的引用全部同步更新。改名是为了和 MiBee 体系下的命名统一,原来的 HomeCam 后缀容易和家居摄像头混在一起。

v0.3.0 的重点是 ONVIF 接入,让这个 MCU 摄像头能被主流 NVR 自动发现:

  • ONVIF WS-Discovery 自动发现:固件实现了 WS-Discovery 的 UDP 多播响应,Synology Surveillance Station、Milestone XProtect 这类支持 ONVIF 的 NVR 不用再手动填 IP,局域网内自动出现。
  • ONVIF SOAP 服务:Probe、DeviceInfo、GetProfiles、StreamURI 等,返回 HTTP MJPEG URI。
  • MJPEG 独立端口:MJPEG 流从原来的 HTTP 80 端口挪到独立的 TCP 81 端口,由单独的服务任务承载——之前 MJPEG 流和 Web UI 共用一个 HTTP server,流任务一忙就把配置页卡死。
  • 帧广播器(Frame Broadcaster):彻底解决了 v0.6.0 测试时反复出现的帧缓存(FB)争用——camera FB 拿到后立即拷贝到 PSRAM 归还,录制和流任务各自持拷贝,互不阻塞。这正是 camera-test-machines 里详细写过的 PSRAM 双缓冲方案的正式落地。

另外补了 Prometheus /metrics(堆内存、PSRAM、温度、录制统计等 15+ 项)、摄像头初始化失败时的优雅降级(无摄像头也能启动,Web UI 显示离线),以及把最小延时摄影间隔降到了 1 秒。RTSP 相关代码整个移除——ONVIF + HTTP MJPEG 覆盖了原来 RTSP 的场景。

rpi-cam → mibee-eye-raspi(仓库迁移 + 定位扩展)

树莓派的 Go ONVIF 摄像头服务 rpi-cam 做了一次比较大的调整:仓库从 raspberrypi-camera 迁到了 mibee-eye-raspi,定位也从「树莓派摄像头」扩展为「所有支持树莓派摄像头模组的主板」。

mermaid
flowchart LR
    REPO["mibee-eye-raspi<br/>Go ONVIF 摄像头服务"] --> ARM["ARM64 主板"]
    ARM --> RPI["树莓派 3B/4/5"]
    ARM --> BP["香蕉派 BPI-M5<br/>(RK3588)"]
    ARM --> OP["香橙派 5<br/>(RK3588)"]
    ARM --> OTH["其它 libcamera<br/>兼容主板"]

    classDef repo fill:#E3F2FD,stroke:#1565C0,color:#1565C0
    classDef arm fill:#FFF3E0,stroke:#E65100,color:#BF360C
    classDef board fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
    class REPO repo
    class ARM arm
    class RPI,BP,OP,OTH board

关键点是这些主板都走 libcamera 栈——rpi-cam 的数字 PTZ(基于 libcamera 的 ScalerCrop)、成像控制、H.264 推流本来就是基于 libcamera 实现的,所以在香蕉派 M5(RK3588)、香橙派 5 这类同样支持 libcamera 的 ARM64 板子上的行为完全一致。RK3588 这类板子还能用上硬件 H.265 编码,性能比树莓派 3B 强不少。

改名和迁仓库是为了把这个事实体现出来——它不再是「树莓派专用」,而是「任何能跑 libcamera 的 ARM 板」都能用的 ONVIF 参考实现。功能本身和 v0.2.0 一致(HLS 直播、Web Admin UI、SPS/PPS 缓存快照),主要是定位和分发渠道的变化。

MiBeeCam(v0.2.2)

基于 Luatos ESP32-S3 A10 模块的紧凑型摄像头,这版主要修了 camera-test-machines 里提到的那个调试遗留 bug——wifi_start_sta() 里硬编码的测试 WiFi 凭证,参数被 (void) 抑制后完全忽略。v0.2.2 移除了硬编码凭据,函数参数正常生效。同时把 ESP-IDF 版本引用更新到 v5.5.4,补了 WiFi STA 故障排查文档。

相关链接