livehal
libcamera / V4L2 / RDK X5 原生采集与编码管线的架构和构建指南。
livehal 是 Hardware Abstraction Layer(硬件抽象层)crate,封装了 native-pipeline C++ pipeline。它向 liveion 暴露安全的 Rust API(NativePipeline),同时将所有 FFI 细节保持在 crate 内部。
架构
┌──────────────────────────────────────────────────────────┐
│ liveion(RTP / WHEP / 源管理器) │
│ │
│ NativeSource(统一薄封装层) │
│ │ │
│ ▼ │
│ NativeEncodedSource │
│ webrtc-rs H264Payloader / Packetizer │
│ → MediaPacket::RtpPacket(Arc<Packet>) │
│ → inject_rtp(无 marshal/unmarshal 往返) │
│ │ │
│ RTCP PLI → request_keyframe() │
│ │ │
│─────────│─ 可选依赖边界 ──────────────────────────────────│
│ ▼ │
│ livehal(原生后端 crate) │
│ │
│ NativePipeline(安全 Rust 封装) │
│ │ │
│ ▼ crate-private FFI │
│ native_ffi.rs → source_pipeline_ffi.h │
│ │ │
│ ▼ │
│ C++(native-pipeline) │
│ │
│ SourcePipeline │
│ ├─ CaptureBackend → RawFrame(C++ 内部) │
│ └─ EncoderBackend → EncodedPacket │
│ │ │
│ ▼ FFI 回调 │
│ on_encoded_packet() ← EncodedPacketFFI │
│ │ │
│ ▼ 数据立即拷贝 → mpsc 通道 │
│ EncodedPacket → liveion NativeEncodedSource │
└──────────────────────────────────────────────────────────┘- RawFrame 仅在 C++ 内部使用,不会跨越 FFI 边界。
- EncodedPacket 通过
livehal内的纯 C 回调跨越 FFI;数据会被立即拷贝,并通过 mpsc 通道发送给liveion。 - 所有 FFI 细节在
livehal内部都是 crate-private 的;liveion只能通过通道看到EncodedPacket。 - 原生源的 RTP 路径:
EncodedPacket→ webrtc-rsH264Payloader/Packetizer→MediaPacket::RtpPacket(Arc<Packet>)→track.inject_rtp。这避免了其他源所使用的Packet→ bytes →Packet::unmarshal往返。 MediaPacket::Rtp { data }字节路径仍由rtp_listener/rtsp_source/sdp_source使用。- DMA-BUF 零拷贝:已在通用 V4L2 → RKMPP 与树莓派(libcamera / 通用 V4L2 → V4L2 M2M)上实现。采集和编码同时设
prefer_dmabuf = true时,采集侧把每帧的 DMA-BUF 直接交给编码器入队——整帧不再经过 CPU 拷贝。缓冲区所有权是显式的:从出队到该帧编码完成期间缓冲区归编码器所有,待硬件读取完毕后才 requeue 回去(延迟 requeue 释放契约,摄像头不可能在编码中途覆盖缓冲区)。导出或导入失败(或帧布局无法被编码器导入)时回退 CPU 拷贝路径。RDK 后端尚未实现 DMA-BUF 导入(encoder_rdk.cpp会拒绝BufferKind::DmaBuf),该路径仍以 CPU 拷贝为默认。
配置
源配置在 conf/live777.toml 中按 stream 划分,写在 [[stream.<name>.sources]] 下。 [stream] 下的每个键就是 stream 名称;每个 stream 可以选配一个 DataChannel <-> UDP 通道。每个 stream 同时只能运行一个源——源注册表以流名为键, 因此请为每个流只配置一个源。
预注册流与按需源(on-demand)
每个 [stream.<name>] 条目都是"预注册"(provisioned)的:流在启动时即注册, 即使空闲也始终出现在 API 和 Dashboard 中,不受自动回收策略影响 (orphan reaper、auto_delete_whip / auto_delete_whep), 也不能通过 admin API 创建或删除(POST / DELETE /api/streams/<name> 返回 409)。
预注册流是永久的,但其媒体面仍跟随推流生命周期:
- WHIP 推流端正常离开时,流会被重置为待机:其订阅者全部断线 (WHEP 无法重协商 track,观众需重新订阅),并触发一对 reason 为
reset的stream-deleted+stream-createdhook。常驻源会重启; on-demand 源保持停止直到下一个订阅者到来。 - 配置源运行期间 WHIP 推流端无法挂载(反之亦然)——否则两个推流方的 track 会混入所有订阅者,该推流请求返回 409。
默认情况下,流的源在服务器启动时无条件启动。设置 on_demand = true 后, 源只在有人观看时运行——摄像头 / 编码器 / RTSP 拉流在第一个订阅者 (WHEP、cascade push 或 RTSP 拉流)到来时才启动,在最后一个订阅者离开后停止:
[stream.cam1]
on_demand = true
# 最后一个订阅者离开后停止源的宽限时间(毫秒,默认 10000)
on_demand_close_after_ms = 10000
# 第一个订阅者等待源就绪的最长时间,超时后订阅请求失败(毫秒,默认 10000)
on_demand_start_timeout_ms = 10000
[[stream.cam1.sources]]
url = "rtsp://192.168.1.100:554/stream"在源启动进行期间到达的订阅者会等待启动完成(以及彼此),而不是拿到一个 不含 track 的应答;若源在 on_demand_start_timeout_ms 内未就绪,订阅请求 失败,客户端可重试。源的启停会以 virtual-source 会话 ID 发出 PublishStarted / PublishStopped 事件,同时驱动录制 (recorder.auto_streams)和 on_publish_started / on_publish_stopped 钩子。
WHEP URL 源的有效源就绪等待时间至少为 35000ms,即使 on_demand_start_timeout_ms 配得更低也是如此。这样冷启动的上游 WHEP/on-demand 源可以完成 HTTP setup 超时预算并送出第一包媒体。
on-demand 流在空闲时 Dashboard 显示 standby 徽标,源运行时显示 on-demand;其他预注册流显示 config 徽标。
基于 URL 的方式(非原生:RTSP / SDP / RTP)
[stream.rtsp-cam]
[[stream.rtsp-cam.sources]]
url = "rtsp://192.168.1.100:554/stream"当 SDP 文件源的连接地址是组播组时(例如 c=IN IP4 230.1.1.1), 源会在每个媒体端口上加入该组播组,而不仅仅是绑定端口——无人机、 机器人相机等常开组播发送端可以直接接入 live777;配合 on_demand = true 时,只有存在观看者期间才保持组成员关系:
[stream.robot-cam]
on_demand = true
[[stream.robot-cam.sources]]
url = "/etc/live777/robot-cam.sdp" # c=IN IP4 230.1.1.1, m=video 1720 RTP/AVP 96
# 加入组播组使用的网卡:IPv4 组填 IPv4 地址,IPv6 组填网卡序号或名称
# (此项未配置时,取 SDP 地址自带的 `%zone`,如 `c=IN IP6 ff12::1%eth0`)。
# 不配置则由内核选择,只能收到默认路由网卡上的组播流量——链路本地
# IPv6 组(ff02::/16、ff12::/16)必须显式指定网卡。
multicast_interface = "192.168.123.11"连接地址的 RFC 4566 后缀(c=IN IP4 230.1.1.1/64、c=IN IP6 ff15::1/3) 会被正确识别。接收 socket 设置了 SO_REUSEADDR,同一台主机上可以有 多个接收者同时收同一个 group:port(另一条流、监控工具)。绑定/加入 失败会先按退避重试若干次再判定源启动失败——但对于常开的(非 on_demand)组播流,仍应确保 live777 在网卡地址分配完成后再启动 (systemd:After=network-online.target),因为较长的 DHCP 延迟会 超过重试窗口。
WARNING
组播发送端保留内核默认的回环行为(IP_MULTICAST_LOOP),因此本源 也会收到同机发送端——包括 live777 自己的 rtp:// 输出 target—— 发往已加入 group:port 的流量。在同一台主机上把源和 target 指向同一 个 group:port(直接配置,或经 liveman 级联绕回本机)会让 live777 摄入自己的输出形成反馈回环;同机演示请使用不同的组播组或端口。
结构化原生配置(libcamera / V4L2 / RDK)
[stream.pi-cam]
[[stream.pi-cam.sources]]
[stream.pi-cam.sources.capture]
backend = "libcamera"
device = "0"
width = 640
height = 480
fps = 30
pixel_format = "yuv420"
# hflip = true # 左右镜像;仅 libcamera(ISP 完成,零 CPU 开销)
# vflip = true # 上下翻转;hflip + vflip = 旋转 180°
[stream.pi-cam.sources.encoder]
backend = "v4l2-m2m"
codec = "h264"
bitrate = 1_000_000
profile = "baseline" # 也可以是 6 位十六进制 profile-level-id,如 "42001f"
level = "3.1" # 当 profile 为名称时必填
gop = 60 # 关键帧间隔(帧数);0 = 仅按需出关键帧
# (PLI/FIR、新订阅者加入)——仅 v4l2-m2m
# bitrate_mode = "vbr" # 仅 v4l2-m2m:"vbr"(驱动默认)或 "cbr";
# 注意 bcm2835-codec 固件会拒绝 CBR
[stream.pi-cam.sources.output]
payload_type = 96
clock_rate = 90000pixel_format 和 codec 值在启动时就会被校验(未知值会尽早报错)。capture + encoder 与 url 互斥,源类型由 capture.backend 推导(device 对 libcamera 是 camera ID,对 v4l2 是设备路径)。
capture.hflip / capture.vflip(默认均为 false)用于画面翻转—— 分别是左右镜像和上下翻转;两者同时开启即旋转 180°。仅 libcamera 后端 支持:翻转由 ISP 完成,零 CPU 开销。v4l2 后端在启动校验时会直接 报错——它的视频节点(unicam)只做 DMA 搬运、没有图像处理能力,而翻转 control 只存在于 sensor 子设备节点上,绕开 libcamera 手动设置会与 ISP 的 Bayer 顺序失配。不提供 90°/270° 旋转:树莓派 ISP 硬件不支持转置。
conf/live777.toml 自带注释掉的 Pi / RDK 示例。复制它们到你自己的配置中即可启用摄像头源。
自适应码率(实验性)
原生编码源默认开启自适应码率 —— 运行中的编码器目标码率由 WHEP 订阅端的 RTCP 反馈驱动(issue #409):每秒采样一次丢包(优先 TWCC, 否则用 Receiver Report 的 fraction_lost),由 AIMD 控制器在运行时调整 编码器。无需任何配置键;只有两个可选形态:关闭和自定义下限:
[stream.pi-cam.sources.encoder]
bitrate = 4_000_000 # 上限 —— 控制器只从这里往下调
# adaptive = false # 严格固定码率(关闭 AIMD)
# adaptive = { min_bitrate = 500_000 }
# 自定义下限(默认:声明了档位时取最低档,
# 否则 max(bitrate / 8, 300_000))语义:丢包连续两个窗口超过 5% 就把码率降 15%;持续 10 秒无丢包则回升目标 码率的 5%。多个订阅者时按最差链路决定 —— 共享编码器会被一起拉低,单个弱网 订阅者会拖累整条流(根本解法是 simulcast;不能接受时设 adaptive = false)。 加入不到 5 秒的订阅者不纳入统计(解码器启动期的丢包属正常)。运行时调码率 目前支持 rkmpp 和 v4l2-m2m 后端;其他后端会打印警告并自动停用控制器。
码率遥测与质量档位
GET /api/sources/:streamId/bitrate 报告该源编码器码率当前的驱动 方式 —— adaptive(上面的 AIMD 控制器)或 fixed —— 以及当前 码率(见 HTTP API 文档)。控制通过质量档位 进行:命名的源参数预设,通过 POST /api/sources/:streamId/tier 应用。档位是源自身结构的部分覆盖 —— 可选的 capture 和 encoder 块里放要改的参数;未写的参数保持配置值:
[stream.pi-cam.sources.encoder]
bitrate = 4_000_000 # AIMD 的启动上限
adaptive = { min_bitrate = 300_000 } # 可选;AIMD 默认开启
# 纯编码器档:原地调运行中的编码器,无缝切换
[[stream.pi-cam.sources.tiers]]
name = "mid"
encoder = { bitrate = 2_000_000 }
# 阶梯档:采集几何 + 编码预算,通过重建采集+编码管线切换
# (亚秒级断帧,订阅者保持连接)。这里省略 encoder.bitrate ——
# 按 640x480x15 以 ~0.07 bpp 推导(约 645 kbps)
[[stream.pi-cam.sources.tiers]]
name = "low"
capture = { width = 640, height = 480, fps = 15 }档位放在 capture/encoder 同级(而不是 encoder 块内),因为一个 档位横跨两个块:encoder 覆盖调编码器,而 capture 覆盖要改采集 配置(livehal 没有 scaler 环节 —— 采集尺寸就是编码器输入尺寸, 帧率也是采集侧属性)。纯编码器档原地无缝切换;动了采集几何的档位 会在流的下方重建管线 —— RTP 会话保持存活,订阅者只会看到 短暂定格和一次带内 SPS/PPS 变更。capture.width/height 必须成对 设置,可以是传感器支持的任意尺寸 —— 包括高于启动配置(档位阶梯的 顶端不必是启动档,例如 972p30 启动配 1080p20 最高档);capture.fps 同样可以是传感器支持的任意帧率(低延时档可以用分辨率换帧率,例如 OV5647 上的 480p60 档 —— 不支持的模式会在应用时被拒绝并回滚)。 encoder.bitrate 同样可以高于启动的 encoder.bitrate(仅受编码器 有符号 32 位控制范围约束),因此阶梯顶端可以携带比启动档更高的码率 —— 而且它是可选的:省略时按档位的生效几何以 ~0.07 bpp 推导 (下限 50 kbps),手工调优的阶梯通常也落在这个附近;内容特殊(如 运动场景)或想在同分辨率出高清/经济两档时再显式指定。空档位 (什么都没设置)会被拒绝。
应用档位不会挂起自适应控制器:档位的码率成为 AIMD 的新上限, 它会在档内继续跟随网络状况调节。重建失败(例如摄像头拒绝新尺寸) 会回滚到之前的配置;档位的字段总是叠加在配置的采集参数上:未 设置的字段回落到配置值,因此混合阶梯(纯码率档与几何档并存)语义 是明确的。同一机制日后还可以扩展到切换源的类型(例如从摄像头 切到 RTSP 地址)——档位描述的是源配置,而不是编码器内部。
后端命名
| 层级 | 取值 |
|---|---|
capture.backend | "libcamera", "v4l2" |
encoder.backend | "v4l2-m2m", "rdk", "rkmpp" |
pixel_format 取值
| TOML 字符串 | RawPixelFormat | 数值 |
|---|---|---|
yuyv, yuyv422 | Yuyv422 | 0 |
nv12 | Nv12 | 1 |
yuv420, yuv420p | Yuv420p | 2 |
rgb888, rgb | Rgb888 | 4 |
uyvy, uyvy422 | Uyvy422 | 5 |
codec 取值
| TOML 字符串 | VideoCodec | 数值 |
|---|---|---|
h264 | H264 | 100 |
h265, hevc | H265 | 101 |
av1 | Av1 | 102 |
vp8 | Vp8 | 103 |
vp9 | Vp9 | 104 |
特性标志
特性分为采集后端、编码后端和便捷预设三类。所有后端特性都隐含 native-source,而 native-source 又会启用 source(自动启动)和 dep:livehal。
采集后端
| 特性 | 后端 |
|---|---|
capture-libcamera | libcamera(树莓派 CSI 摄像头) |
capture-v4l2 | V4L2 视频采集(USB 摄像头、通用 Linux) |
编码后端
| 特性 | 后端 |
|---|---|
encoder-v4l2-m2m | V4L2 Memory-to-Memory 硬件编码器 |
encoder-rdk | 地平线 RDK X5 硬件编码器 |
encoder-rkmpp | 瑞芯微 MPP 硬件编码器(仅 aarch64) |
平台预设
| 预设 | 展开为 |
|---|---|
native-rpi | capture-libcamera, capture-v4l2, encoder-v4l2-m2m |
native-generic-v4l2 | capture-v4l2, encoder-v4l2-m2m |
native-rdk | capture-v4l2, encoder-rdk |
native-rkmpp | capture-v4l2, encoder-rkmpp |
无需额外加 --features source——预设已经包含自动启动。
# 树莓派 CSI
cargo build --bin live777 --release \
--target aarch64-unknown-linux-gnu \
--no-default-features --features native-rpi,webui
# 通用 Linux V4L2
cargo build --bin live777 --release \
--no-default-features --features native-generic-v4l2,webui
# RDK X5
cargo build --bin live777 --release \
--target aarch64-unknown-linux-gnu \
--no-default-features --features native-rdk,webui
# 瑞芯微 RKMPP(RK3588、RV1126B)
cargo build --bin live777 --release \
--target aarch64-unknown-linux-gnu \
--no-default-features --features native-rkmpp,webui构建
预编译二进制
通常不需要自己编译:自 v0.10.0 起的 Release 会发布原生构建(树莓派用 live777-<版本>-aarch64-unknown-linux-gnu-rpi.tar.gz,RK3588/RV1126B 用 -rkmpp.tar.gz)。见安装部署和 树莓派部署指南。只有需要未发布的改动、或 RDK X5 (无预编译资产,需要 RDK_SYSROOT)时才从源码构建。
前置要求
- CMake ≥ 3.16
- C++17 编译器(gcc 或 clang)
- 按需准备平台 SDK(libcamera、RDK sysroot)
树莓派(libcamera)
cargo build --bin live777 --release \
--target aarch64-unknown-linux-gnu \
--no-default-features --features native-rpi,webui交叉编译时,just rpi-cross-build 默认使用已内置 Raspberry Pi libcamera sysroot 的 GHCR cross 镜像。只有需要覆盖为从设备同步出的 sysroot 时才设置 RPI_SYSROOT。
通用 Linux V4L2
cargo build --bin live777 --release \
--no-default-features --features native-generic-v4l2,webuiRDK X5
cargo build --bin live777 --release \
--target aarch64-unknown-linux-gnu \
--no-default-features --features native-rdk,webui需要带有 hb_media_codec 库的 RDK sysroot。必须设置 RDK_SYSROOT 指向 sysroot 路径;没有默认值。
注意: DMA-BUF 零拷贝编码路径尚未实现。详见上文“架构”章节中的 DMA-BUF 说明。
瑞芯微 RKMPP(RK3588、RV1126B)
cargo build --bin live777 --release \
--target aarch64-unknown-linux-gnu \
--no-default-features --features native-rkmpp,webui需要 Rockchip MPP 库(librockchip_mpp)及头文件。交叉编译时设置 RKMPP_SYSROOT 指向包含它们的 sysroot。rkmpp 编码器仅接受 NV12 输入,请搭配 pixel_format = "nv12" 采集。
注意: rkmpp 的 profile 配置比其他后端更严格。H.264 时
profile必须是 6 位 profile-level-id 十六进制串(如"420028"= Baseline Level 4.0,"640028"= High Level 4.0),且不允许单独的level字段——level 已编码在十六进制串中。H.265 时使用profile = "main",可选level("3.0"–"5.1",默认"4.0");仅支持 Main profile / Main tier。
Cross Images 工作流会发布 ghcr.io/binbat/crossbuilder-aarch64-rkmpp:latest,这是一个内置 MPP sysroot(位于 /opt/rkmpp-sysroot,由 docker/Dockerfile.cross-aarch64-rkmpp 构建)的 aarch64 交叉编译镜像。通过 CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_IMAGE 指定给 cross 使用,或直接运行 just rkmpp-pack-size;设置 RKMPP_SYSROOT 可改用从设备拉取的 sysroot。
macOS(仅开发 / 检查)
cargo check --no-default-features
cargo check --features native-rpi,webui注意: 在 macOS 和 Windows 上,原生后端特性会被构建脚本静默跳过;不会调用 CMake,也不会链接原生符号。你可以用原生特性运行
cargo check做静态检查,但生成的二进制无法在这些平台上使用原生源。
环境变量
| 变量 | 用途 |
|---|---|
RPI_SYSROOT | 可选的树莓派 sysroot,需包含 libcamera-dev;会覆盖 RPi cross 镜像内置的 sysroot。 |
RDK_SYSROOT | 地平线 RDK X5 SDK sysroot 路径。在 aarch64 上构建 encoder-rdk / native-rdk 时必须设置。 |
RKMPP_SYSROOT | 包含 Rockchip MPP 库和头文件的 sysroot 路径。在 aarch64 上构建 encoder-rkmpp / native-rkmpp 时使用。 |
LIVEHAL_CXX_STDLIB | 覆盖要链接的 C++ 标准库(如 stdc++、c++ 等),用于交叉编译工具链。 |
LIVEHAL_RDK_ALLOW_UNDEFINED | 设为 1 可在 sysroot 不完整时允许 RDK 共享库存在未解析符号。 |
后端选择(构建时)
CMake 后端根据启用的采集/编码特性推断:
| 启用的特性 | 选定后端 | CMake 开启的宏 |
|---|---|---|
capture-libcamera | rpi | ENABLE_BACKEND_PI, ENABLE_CAPTURE_LIBCAMERA, ENABLE_CAPTURE_V4L2, ENABLE_ENCODER_V4L2_M2M |
aarch64 上的 encoder-rdk | rdk-x5 | ENABLE_BACKEND_RDK_X5, ENABLE_CAPTURE_V4L2, ENABLE_ENCODER_RDK_X5 |
aarch64 上的 encoder-rkmpp | rkmpp | ENABLE_CAPTURE_V4L2, ENABLE_ENCODER_RKMPP |
capture-v4l2 / encoder-v4l2-m2m | generic-v4l2 | ENABLE_CAPTURE_V4L2, ENABLE_ENCODER_V4L2_M2M |
当没有启用任何 capture-* 特性时,CMake 会被完全跳过。仅启用编码特性不会触发 CMake 构建——SourcePipeline 需要一个采集后端。
capture-libcamera 与 encoder-rdk、encoder-rkmpp 互斥。如果同时启用,构建脚本会发出警告并忽略对应的编码特性,选择 rpi(libcamera)后端。
配置示例
树莓派 CSI
[stream.pi-cam]
[[stream.pi-cam.sources]]
[stream.pi-cam.sources.capture]
backend = "libcamera"
device = "0"
width = 640
height = 480
fps = 30
pixel_format = "yuv420"
[stream.pi-cam.sources.encoder]
backend = "v4l2-m2m"
codec = "h264"
bitrate = 1_000_000
profile = "baseline"
level = "3.1"
gop = 60
[stream.pi-cam.sources.output]
payload_type = 96
clock_rate = 90000树莓派 USB V4L2
使用打包 UYVY 格式的采集设备时,可以设置 pixel_format = "uyvy" (uyvy422 是其别名)。generic V4L2 路径会保留采集 stride,并将 UYVY 原样传递给 V4L2 M2M,因此编码器设备也必须声明支持 UYVY 输入。
[stream.usb-cam]
[[stream.usb-cam.sources]]
[stream.usb-cam.sources.capture]
backend = "v4l2"
device = "/dev/video2"
width = 640
height = 480
fps = 30
pixel_format = "yuyv"
[stream.usb-cam.sources.encoder]
backend = "v4l2-m2m"
codec = "h264"
bitrate = 1_000_000
profile = "42001f"
gop = 60
[stream.usb-cam.sources.output]
payload_type = 96
clock_rate = 90000RDK X5
[stream.rdk-cam]
[[stream.rdk-cam.sources]]
[stream.rdk-cam.sources.capture]
backend = "v4l2"
device = "/dev/video0"
width = 640
height = 480
fps = 30
pixel_format = "yuyv"
[stream.rdk-cam.sources.encoder]
backend = "rdk"
codec = "h264"
bitrate = 1_000_000
profile = "42001f"
gop = 60
[stream.rdk-cam.sources.output]
payload_type = 96
clock_rate = 90000瑞芯微 RKMPP(RK3588 / RV1126B)
[stream.rk-cam]
[[stream.rk-cam.sources]]
[stream.rk-cam.sources.capture]
backend = "v4l2"
device = "/dev/video0"
width = 1280
height = 720
fps = 60
pixel_format = "nv12"
prefer_dmabuf = true # DMA-BUF 零拷贝(采集侧)
[stream.rk-cam.sources.encoder]
backend = "rkmpp"
codec = "h264"
bitrate = 4_000_000
profile = "420028"
gop = 120
prefer_dmabuf = true # DMA-BUF 零拷贝(编码侧)
[stream.rk-cam.sources.output]
payload_type = 96
clock_rate = 90000零拷贝路径需要两侧都设 prefer_dmabuf——语义和各平台支持见 零拷贝(DMA-BUF)。输入必须是 NV12。
零拷贝(DMA-BUF)
启用零拷贝后,采集后端把每帧的 dma-buf 直接交给编码器——帧数据完全不经过 CPU。未启用时,每帧在输入路上被拷贝两次:先在采集侧拷进 CPU staging 缓冲,再拷进编码器输入缓冲。
缓冲区所有权是显式的:采集缓冲区在编码器硬件读完之前不会 requeue(延迟 requeue 释放契约),摄像头不可能在编码中途覆盖帧数据。缓冲区无法导出/导入、或布局与编码器不匹配(如 stride 填充)时,pipeline 逐帧回退为单次拷贝(即零拷贝前的开销),并只打印一次日志。
启用方式:两侧都设 prefer_dmabuf
零拷贝是两个独立端之间的所有权契约,因此两侧都要显式开启:
| 开关 | 允许该侧…… |
|---|---|
capture.prefer_dmabuf | 把缓冲区留在采集队列之外,所有权交给消费方(延迟 requeue) |
encoder.prefer_dmabuf | 导入外部 dma-buf,并持有到硬件读取完毕 |
任一侧未设置时 pipeline 照常运行,只是走 CPU 拷贝路径(绝不报错)。所有支持的平台都用这两个开关配置零拷贝,因此 Rockchip RK3588 / RV1126B 的配置(上文 RKMPP 示例)与树莓派的形状完全一致:
[stream.cam.sources.capture]
backend = "libcamera"
device = "0"
width = 1280
height = 720
fps = 30
pixel_format = "yuv420"
prefer_dmabuf = true # DMA-BUF 零拷贝(采集侧)
[stream.cam.sources.encoder]
backend = "v4l2-m2m"
codec = "h264"
bitrate = 2_000_000
prefer_dmabuf = true # DMA-BUF 零拷贝(编码侧)从启动日志确认是否生效——出现 [V4l2M2mEncoder] zero-copy input enabled 即生效;之后若出现 frame layout not dmabuf-importable 则说明在用拷贝回退(日志会说明是哪个布局参数不匹配)。
平台支持
| 平台 | 采集后端 | 编码后端 | 状态 |
|---|---|---|---|
| 树莓派 | libcamera | v4l2-m2m | 支持 |
| 树莓派 / 通用 Linux | v4l2(单 plane NV12 或 YUV420) | v4l2-m2m | 支持 |
| Rockchip RK3588 / RV1126B | v4l2(NV12) | rkmpp | 支持 |
| RDK X5 | — | rdk | 未实现(始终拷贝) |
libcamera 与 v4l2 采集零拷贝的区别
libcamera | v4l2 | |
|---|---|---|
| 缓冲区来源 | libcamera 分配的 dma-buf(本来就是单 fd) | 驱动 MMAP 缓冲区经 VIDIOC_EXPBUF 导出(需要驱动支持) |
| 可 arm 的布局 | YUV420P——3 个 plane 以 offset 共存于一个 fd | 单 plane 连续 NV12 或 YUV420;多 fd 的 NV12M / YUV420M 保持 CPU 路径 |
| 缓冲区数量 | 12(编码器最多持有 8 个 + ISP 余量) | 16 |
| 像素转换 | 无——ISP 输出原样使用,stride 填充如实暴露 | 可 arm 格式均无转换 |
两种情况下编码器都只在采集 stride 和 plane offset 与自身单 plane 布局完全一致时才导入 dma-buf,否则走 dma_heap 池的单次拷贝回退。树莓派上 ISP 和 bcm2835-codec 都把 stride 按 64 字节对齐,常用宽度(320 / 640 / 1280 / 1296 / 1920)都能干净导入。
实测数据(Raspberry Pi Zero 2 W)
用本版本实测:Debian 13,OV5647,libcamera → v4l2-m2m H.264 baseline 4.0,无订阅者;稳态下单核 CPU 占比:
| 流 | 拷贝路径 | 零拷贝 | 降幅 |
|---|---|---|---|
| 1296x972@30 | ~55% | ~12% | ~4.5x |
| 1920x1080@30 | ~65% | ~12% | ~5x |
| 640x480@60 | ~40% | ~15% | ~2.7x |
| 1280x720@10 | ~17% | ~4.5% | ~3.7x |
| 320x240@10 | ~5.5% | ~3% | ~1.8x |
零拷贝路径上剩下的开销是每帧固定成本(驱动 ioctl、RTP 打包),与像素数无关——所以高帧率、小分辨率的流相对收益最小,而 1080p@30(拷贝路径下很难再同时跑别的任务)变得非常便宜。
树莓派说明
已在 Raspberry Pi Zero 2 W(Debian 13)+ OV5647(v1 摄像头)上实测验证。
Sensor 帧率上限
采集配置里的 fps 是请求值:libcamera 会将其钳制到所选 sensor 模式允许的范围。实测各模式上限(OV5647;模式被选中时 rpicam-hello 启动日志也会打印该上限):
| 模式 | 最高 fps |
|---|---|
| 640x480 | ~62.5 |
| 1296x972 | ~46 |
| 1920x1080 | ~33 |
| 2592x1944 | ~15.6 |
- 60fps 只有 640x480 能达到;请求更高只会按上限运行。120fps 不可能。
- Zero 2 W 上的大致 CPU 开销(libcamera → v4l2-m2m H.264):640x480@30 约单核 20%,640x480@60 约 40%,1296x972@30 约 65%。
- 零拷贝可以消掉其中大部分开销——启用方法、libcamera 与 v4l2 的对比、以及这块板子的各分辨率实测数据见 零拷贝(DMA-BUF)。
低延时推流
glass-to-glass 延时的主导因素是帧节拍而不是编码器速度:每一级(sensor 读出、采集队列、编码器输入、网络、播放器 jitter buffer)最多可以攒住一帧。帧率翻倍,每级的最坏等待减半——30→60fps(33.3 → 16.7ms)端到端通常能省 30–50ms。
代价是曝光:单帧曝光预算是 1/fps(60fps 最多 16.7ms,120fps 最多 8.3ms)。暗光下画面会更暗、噪点更多。树莓派上 pipeline 按 fps 设置 FrameDurationLimits,帧率保持锁定——AE 只能缩短曝光、提高增益(画面变暗/变噪,但延时保住)。
局域网延时预算(树莓派)
目前还没公布树莓派的 glass-to-glass 实测值——可用二维码计时法自行测量(摄像头拍摄高刷屏上的毫秒计时器,拍照对比 WHEP 播放画面与计时器读数)。640x480@60 局域网下的预算构成:
- sensor 节拍 + ISP + 采集:1~2 个帧周期(60fps 时 17~33ms)。
- v4l2-m2m 编码器:每帧几 ms;VGA 60fps 在 Zero 2 W 上余量充足(约单核 40%)。
- 局域网网络:同网段 host 候选 ICE,几 ms。
- 浏览器播放器:通常占比最大——Chromium 的 jitter buffer、解码和渲染一般占去一半以上。
所以在帧率之后,剩下的主要优化空间在播放器侧(解码/渲染路径和 jitter buffer),而不是服务端。
60fps 参考配置(树莓派)
[stream.cam]
[[stream.cam.sources]]
[stream.cam.sources.capture]
backend = "libcamera"
device = "0"
width = 640
height = 480
fps = 60 # OV5647 VGA 模式上限约 62.5
pixel_format = "yuv420"
[stream.cam.sources.encoder]
backend = "v4l2-m2m"
codec = "h264"
bitrate = 2_000_000
profile = "baseline"
level = "3.1"
gop = 120 # 60fps 下 2 秒;GOP 越短,加入/恢复越快
[stream.cam.sources.output]
payload_type = 96
clock_rate = 90000- 用编码器统计行(
[V4l2M2mEncoder] stats: injected=…)确认真实帧率:它按实际采集 fps 增长。 - 播放器尽量与服务端同网段(host 候选 ICE,
ice_udp_addrs = ["auto"]),避免中继绕行。 - 浏览器 WHEP 客户端应在 POST 前完成 ICE 候选收集(完整 SDP offer);纯 trickle 的客户端目前连不上——见 issue #417。