perf(video): 新增 RKMPP DMA 采集编码通路并完善恢复逻辑

支持原生 HDMI 和 UVC 缓冲区导出,增加同步 RKMPP 编码及可选 MJPEG 硬件转码。
校验帧布局和缓冲区租约,在 DMA 不可用或编码失败时回退到复制通路。
保留自定义码率和 GOP 策略,重开采集时同步 HDMI 源帧率,并区分 UVC 超时状态。

验证:88 个视频测试通过(含 4 个新增回归测试);ARM64 cargo check --tests 通过。
This commit is contained in:
mofeng-git
2026-09-05 20:55:30 +08:00
parent 620fe0be54
commit db9d79554a
11 changed files with 1680 additions and 38 deletions

View File

@@ -5,6 +5,11 @@ pub mod ffmpeg;
#[cfg(any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp"))]
pub mod ffmpeg_hw;
pub mod ffmpeg_ram;
#[cfg(all(
target_os = "linux",
any(target_arch = "aarch64", target_arch = "arm", feature = "rkmpp")
))]
pub mod rkmpp_dmabuf;
#[no_mangle]
pub extern "C" fn hwcodec_log(level: i32, message: *const std::os::raw::c_char) {

View File

@@ -0,0 +1,193 @@
//! Synchronous RKMPP encoder for pre-exported V4L2 DMA buffers.
//! Unlike the byte-slice encoder this never reads raw pixels on the CPU.
use std::ffi::{c_char, c_int, c_void, CStr};
use std::os::fd::{AsRawFd, OwnedFd};
use std::ptr::NonNull;
unsafe extern "C" {
fn rkmpp_dma_new(
width: c_int,
height: c_int,
stride: c_int,
format: c_int,
codec: c_int,
fps: c_int,
kbps: c_int,
gop: c_int,
fds: *const c_int,
sizes: *const usize,
count: usize,
) -> *mut c_void;
fn rkmpp_dma_encode(
encoder: *mut c_void,
index: usize,
bytes_used: usize,
fresh_fd: c_int,
pts_us: i64,
force_idr: c_int,
data: *mut *const u8,
size: *mut usize,
) -> c_int;
fn rkmpp_dma_reconfigure(encoder: *mut c_void, kbps: c_int, gop: c_int) -> c_int;
fn rkmpp_dma_free(encoder: *mut c_void);
fn rkmpp_dma_error() -> *const c_char;
}
#[derive(Debug, Clone, Copy)]
#[repr(i32)]
pub enum DmaFormat {
Nv12 = 0,
Bgr24 = 1,
Yuyv = 2,
Rgb24 = 3,
Mjpeg = 4,
}
pub struct DmaEncoderConfig {
pub width: u32,
pub height: u32,
pub stride: u32,
pub format: DmaFormat,
pub hevc: bool,
pub fps: u32,
pub bitrate_kbps: u32,
pub gop: u32,
}
pub struct DmaEncoder {
ctx: NonNull<c_void>,
// Export FDs remain open until AFTER mpp_destroy and imported buffer release.
_buffers: Vec<(OwnedFd, usize)>,
// Keep refreshed exports alive until native replacement/release has ended
// all references to the previous import. At most one FD per capture slot.
fresh_buffers: Vec<Option<OwnedFd>>,
}
// Exclusive ownership: the context can move between threads but all calls are sequential.
unsafe impl Send for DmaEncoder {}
fn last_error() -> String {
unsafe {
CStr::from_ptr(rkmpp_dma_error())
.to_string_lossy()
.into_owned()
}
}
impl DmaEncoder {
pub fn new(config: DmaEncoderConfig, buffers: Vec<(OwnedFd, usize)>) -> Result<Self, String> {
let fds: Vec<_> = buffers.iter().map(|(fd, _)| fd.as_raw_fd()).collect();
let sizes: Vec<_> = buffers.iter().map(|(_, size)| *size).collect();
for value in [
config.width,
config.height,
config.stride,
config.fps,
config.bitrate_kbps,
config.gop,
] {
if value > c_int::MAX as u32 {
return Err("DMA encoder parameter overflow".into());
}
}
let ptr = unsafe {
rkmpp_dma_new(
config.width as _,
config.height as _,
config.stride as _,
config.format as c_int,
config.hevc as _,
config.fps as _,
config.bitrate_kbps as _,
config.gop as _,
fds.as_ptr(),
sizes.as_ptr(),
buffers.len(),
)
};
Ok(Self {
ctx: NonNull::new(ptr).ok_or_else(last_error)?,
fresh_buffers: (0..buffers.len()).map(|_| None).collect(),
_buffers: buffers,
})
}
/// # Safety
/// The indexed buffer must be dequeued and exclusively leased to this call.
/// Do not requeue/write it until this function returns. On failure native MPP
/// is synchronously destroyed before returning, ending all input access.
/// `bytes_used` is the actual DQBUF payload length, not the buffer capacity.
/// A refreshed FD, if supplied, must refer to the same leased capture slot
/// with the capacity registered at construction. Ownership is retained here.
pub unsafe fn encode(
&mut self,
index: usize,
bytes_used: usize,
fresh_fd: Option<OwnedFd>,
pts_ms: i64,
force_idr: bool,
) -> Result<Vec<u8>, String> {
let mut data = std::ptr::null();
let mut size = 0;
if index >= self.fresh_buffers.len() {
return Err("Invalid DMA capture index".into());
}
let ret = unsafe {
rkmpp_dma_encode(
self.ctx.as_ptr(),
index,
bytes_used,
fresh_fd.as_ref().map_or(-1, AsRawFd::as_raw_fd),
pts_ms.saturating_mul(1000),
force_idr as _,
&mut data,
&mut size,
)
};
if fresh_fd.is_some() {
// Native has now released the previous import, or destroyed both
// hardware contexts on error. Only now may its old FD be closed.
self.fresh_buffers[index] = fresh_fd;
}
if ret != 0 {
return Err(last_error());
}
if data.is_null() || size == 0 {
return Err("Empty RKMPP DMA packet".into());
}
// Copy only the compressed output, releasing the driver's packet promptly
// regardless of how long a network subscriber retains its Bytes.
Ok(unsafe { std::slice::from_raw_parts(data, size) }.to_vec())
}
pub fn reconfigure(&mut self, kbps: u32, gop: u32) -> Result<(), String> {
if kbps > c_int::MAX as u32 || gop > c_int::MAX as u32 {
return Err("DMA encoder parameter overflow".into());
}
if unsafe { rkmpp_dma_reconfigure(self.ctx.as_ptr(), kbps as _, gop as _) } != 0 {
return Err(last_error());
}
Ok(())
}
}
impl Drop for DmaEncoder {
fn drop(&mut self) {
unsafe { rkmpp_dma_free(self.ctx.as_ptr()) };
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_discriminants_match_native_abi() {
assert_eq!(DmaFormat::Nv12 as c_int, 0);
assert_eq!(DmaFormat::Bgr24 as c_int, 1);
assert_eq!(DmaFormat::Yuyv as c_int, 2);
assert_eq!(DmaFormat::Rgb24 as c_int, 3);
assert_eq!(DmaFormat::Mjpeg as c_int, 4);
}
}