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

@@ -488,6 +488,7 @@ mod ffmpeg {
}
}
builder.file(ffmpeg_hw_dir.join("ffmpeg_hw_mjpeg_h26x.cpp"));
builder.file(ffmpeg_hw_dir.join("rkmpp_dmabuf.cpp"));
} else {
println!(
"cargo:info=Skipping ffmpeg_hw_mjpeg_h26x.cpp (RKMPP) for arch {}",

View File

@@ -0,0 +1,38 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// Validate bounded JPEG headers before giving hardware a fixed-size output
// buffer. Only baseline 8-bit JPEG is accepted; other streams use copy fallback.
// No scan-data traversal or full-packet copy is needed.
inline bool rkmpp_dma_jpeg_header(const uint8_t *data, size_t size, int width, int height) {
if (!data || size < 4 || data[0] != 0xff || data[1] != 0xd8) return false;
size_t pos = 2;
bool sof = false;
while (pos < size) {
if (data[pos++] != 0xff) return false;
while (pos < size && data[pos] == 0xff) ++pos;
if (pos == size) return false;
const unsigned marker = data[pos++];
if (!marker || marker == 0xd8 || marker == 0xd9 || marker == 1 ||
(marker >= 0xd0 && marker <= 0xd7)) return false;
if (size - pos < 2) return false;
const size_t length = (size_t(data[pos]) << 8) | data[pos + 1];
if (length < 2 || length > size - pos) return false;
if (marker == 0xc0) {
if (sof || length < 8 || data[pos + 2] != 8) return false;
const unsigned h = (unsigned(data[pos + 3]) << 8) | data[pos + 4];
const unsigned w = (unsigned(data[pos + 5]) << 8) | data[pos + 6];
const unsigned components = data[pos + 7];
if (w != unsigned(width) || h != unsigned(height) ||
(components != 1 && components != 3) || length != 8 + 3 * components) return false;
sof = true;
} else if (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 && marker != 0xcc) {
return false; // Progressive, lossless, extended or differential SOF.
} else if (marker == 0xda) {
return sof && length >= 6 && size - pos > length;
}
pos += length;
}
return false;
}

View File

@@ -0,0 +1,329 @@
#include "rkmpp_dmabuf_ffi.h"
#include <array>
#include <cstdio>
#include <limits>
#include <new>
#include "rkmpp_dma_jpeg.h"
// Native MPP is already linked by the ARM FFmpeg/RKMPP build. Keep this
// optional for toolchains which only supply the FFmpeg headers.
#if defined(__linux__) && __has_include(<rockchip/rk_mpi.h>)
#define HAVE_MPP_DMA 1
#include <cerrno>
#include <linux/dma-buf.h>
#include <sys/ioctl.h>
extern "C" {
#include <rockchip/rk_mpi.h>
#include <rockchip/mpp_buffer.h>
#include <rockchip/mpp_frame.h>
#include <rockchip/mpp_packet.h>
#include <rockchip/mpp_task.h>
#include <rockchip/rk_venc_cfg.h>
#include <rockchip/rk_venc_rc.h>
}
#endif
static thread_local char dma_error[192] = {};
static int fail(const char *operation, int code) {
std::snprintf(dma_error, sizeof(dma_error), "%s (ret=%d)", operation, code);
return -1;
}
#ifdef HAVE_MPP_DMA
struct RkmppDmaEncoder {
MppCtx ctx = nullptr;
MppApi *api = nullptr;
MppEncCfg cfg = nullptr;
MppPacket packet = nullptr;
std::array<MppBuffer, 16> buffers{};
std::array<size_t, 16> capacities{};
size_t count = 0;
size_t minimum = 0;
int width = 0, height = 0, stride = 0;
MppFrameFormat format = MPP_FMT_YUV420SP;
bool jpeg = false;
MppCtx decoder = nullptr;
MppApi *dec_api = nullptr;
MppBufferGroup decoded_group = nullptr;
MppBuffer decoded_buffer = nullptr;
MppFrame decoded_frame = nullptr;
MppPacket input_packet = nullptr;
bool decoded_layout_set = false;
int decoded_stride = 0, decoded_vstride = 0;
void close() {
if (packet) mpp_packet_deinit(&packet);
if (ctx) {
// A timeout must not expose still-in-use input to V4L2 QBUF.
api->reset(ctx);
mpp_destroy(ctx);
ctx = nullptr;
}
// On any decoder failure, end hardware access before releasing the
// input packet, exported buffers, or allowing the caller's QBUF.
if (decoder) {
dec_api->reset(decoder);
mpp_destroy(decoder);
decoder = nullptr;
}
if (input_packet) mpp_packet_deinit(&input_packet);
if (decoded_frame) mpp_frame_deinit(&decoded_frame);
if (decoded_buffer) { mpp_buffer_put(decoded_buffer); decoded_buffer = nullptr; }
if (decoded_group) { mpp_buffer_group_put(decoded_group); decoded_group = nullptr; }
for (auto &buffer : buffers) {
if (buffer) { mpp_buffer_put(buffer); buffer = nullptr; }
}
if (cfg) { mpp_enc_cfg_deinit(cfg); cfg = nullptr; }
}
~RkmppDmaEncoder() { close(); }
};
static bool set_cfg(RkmppDmaEncoder *e, const char *key, int value) {
int ret = mpp_enc_cfg_set_s32(e->cfg, key, value);
if (ret) fail(key, ret);
return ret == 0;
}
extern "C" int rkmpp_dma_reconfigure(RkmppDmaEncoder *e, int kbps, int gop) {
if (!e || !e->ctx || kbps <= 0 || kbps > 1000000 || gop <= 0)
return fail("invalid DMA encoder configuration", -1);
const int bps = kbps * 1000;
if (!set_cfg(e, "rc:bps_target", bps) ||
!set_cfg(e, "rc:bps_max", bps + bps / 16) ||
!set_cfg(e, "rc:bps_min", bps - bps / 16) ||
!set_cfg(e, "rc:gop", gop)) return -1;
int ret = e->api->control(e->ctx, MPP_ENC_SET_CFG, e->cfg);
return ret ? fail("MPP_ENC_SET_CFG", ret) : 0;
}
extern "C" RkmppDmaEncoder *rkmpp_dma_new(
int width, int height, int stride, int format, int codec, int fps,
int kbps, int gop, const int *fds, const size_t *sizes, size_t count) {
if (width <= 0 || height <= 0 || width > 8192 || height > 8192 ||
(width & 1) || (height & 1) || format < 0 || format > 4 ||
codec < 0 || codec > 1 || fps <= 0 || fps > 240 ||
(format != 4 && stride < width * (format == 1 || format == 3 ? 3 : format == 2 ? 2 : 1)) ||
(format == 2 && stride % 16 != 0) || !fds || !sizes || !count || count > 16) {
fail("invalid DMA frame layout", -1); return nullptr;
}
auto *e = new (std::nothrow) RkmppDmaEncoder;
if (!e) { fail("allocate DMA encoder", -1); return nullptr; }
e->width = width; e->height = height; e->stride = stride; e->count = count;
e->jpeg = format == 4;
if (e->jpeg) e->stride = stride = (width + 15) & ~15;
const int vstride = e->jpeg ? (height + 15) & ~15 : height;
e->format = format == 3 ? MPP_FMT_RGB888 : format == 2 ? MPP_FMT_YUV422_YUYV : format == 1 ? MPP_FMT_BGR888 : MPP_FMT_YUV420SP;
auto abort_init = [e](const char *op, int ret) -> RkmppDmaEncoder * {
fail(op, ret); delete e; return nullptr;
};
int ret = mpp_create(&e->ctx, &e->api);
if (ret) return abort_init("mpp_create", ret);
RK_S64 timeout = 2000;
ret = e->api->control(e->ctx, MPP_SET_OUTPUT_TIMEOUT, &timeout);
if (ret) return abort_init("MPP_SET_OUTPUT_TIMEOUT", ret);
ret = e->api->control(e->ctx, MPP_SET_INPUT_TIMEOUT, &timeout);
if (ret) return abort_init("MPP_SET_INPUT_TIMEOUT", ret);
ret = mpp_init(e->ctx, MPP_CTX_ENC, codec ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC);
if (ret) return abort_init("mpp_init", ret);
ret = mpp_enc_cfg_init(&e->cfg);
if (ret) return abort_init("mpp_enc_cfg_init", ret);
ret = e->api->control(e->ctx, MPP_ENC_GET_CFG, e->cfg);
if (ret) return abort_init("MPP_ENC_GET_CFG", ret);
if (!set_cfg(e, "prep:width", width) || !set_cfg(e, "prep:height", height) ||
!set_cfg(e, "prep:hor_stride", stride) || !set_cfg(e, "prep:ver_stride", vstride) ||
!set_cfg(e, "prep:format", e->format) || !set_cfg(e, "rc:mode", MPP_ENC_RC_MODE_CBR) ||
!set_cfg(e, "rc:fps_in_flex", 0) || !set_cfg(e, "rc:fps_in_num", fps) ||
!set_cfg(e, "rc:fps_in_denorm", 1) || !set_cfg(e, "rc:fps_out_flex", 0) ||
!set_cfg(e, "rc:fps_out_num", fps) || !set_cfg(e, "rc:fps_out_denorm", 1) ||
!set_cfg(e, "codec:type", codec ? MPP_VIDEO_CodingHEVC : MPP_VIDEO_CodingAVC)) {
delete e; return nullptr;
}
// Match the browser-friendly baseline profile used by the existing RKMPP
// byte encoder, rather than inheriting MPP's High-profile default.
const int level = int64_t(width) * height * fps <= int64_t(1920) * 1080 * 60 ? 42 : 52;
if (!codec && (!set_cfg(e, "h264:profile", 66) || !set_cfg(e, "h264:level", level) ||
!set_cfg(e, "h264:cabac_en", 0) || !set_cfg(e, "h264:trans8x8", 0))) {
delete e; return nullptr;
}
if (rkmpp_dma_reconfigure(e, kbps, gop)) { delete e; return nullptr; }
MppEncHeaderMode mode = MPP_ENC_HEADER_MODE_EACH_IDR;
ret = e->api->control(e->ctx, MPP_ENC_SET_HEADER_MODE, &mode);
if (ret) return abort_init("MPP_ENC_SET_HEADER_MODE", ret);
// Reject arithmetic overflow even on 32-bit ARM; stride is supplied by a driver.
if (size_t(stride) > std::numeric_limits<size_t>::max() / size_t(height))
return abort_init("DMA buffer size overflow", -1);
size_t minimum = size_t(stride) * height;
if (format == 0) {
if (minimum > std::numeric_limits<size_t>::max() / 3)
return abort_init("DMA buffer size overflow", -1);
minimum = minimum * 3 / 2;
}
if (e->jpeg) minimum = 68; // SOI/payload plus bounded hardware read headroom.
e->minimum = minimum;
for (size_t i = 0; i < count; ++i) {
if (fds[i] < 0 || sizes[i] < minimum) return abort_init("short DMA buffer", -1);
MppBufferInfo info{};
info.type = MPP_BUFFER_TYPE_EXT_DMA; info.fd = fds[i];
info.size = sizes[i]; info.index = static_cast<int>(i);
ret = mpp_buffer_import(&e->buffers[i], &info);
if (ret) return abort_init("mpp_buffer_import", ret);
e->capacities[i] = sizes[i];
}
if (e->jpeg) {
ret = mpp_create(&e->decoder, &e->dec_api);
if (ret) return abort_init("mpp_create JPEG decoder", ret);
ret = mpp_init(e->decoder, MPP_CTX_DEC, MPP_VIDEO_CodingMJPEG);
if (ret) return abort_init("mpp_init JPEG decoder", ret);
MppFrameFormat output = MPP_FMT_YUV420SP;
ret = e->dec_api->control(e->decoder, MPP_DEC_SET_OUTPUT_FORMAT, &output);
if (ret) return abort_init("JPEG NV12 output", ret);
ret = mpp_buffer_group_get_internal(&e->decoded_group, MPP_BUFFER_TYPE_DRM);
if (ret) return abort_init("JPEG output buffer group", ret);
// MPP JPEG requires aligned storage; reserve the conservative size used
// by its advanced-task decoder demo. One output reused after encode.
ret = mpp_buffer_get(e->decoded_group, &e->decoded_buffer, size_t(stride) * vstride * 4);
if (ret) return abort_init("JPEG output buffer", ret);
ret = mpp_frame_init(&e->decoded_frame);
if (ret) return abort_init("JPEG output frame", ret);
mpp_frame_set_buffer(e->decoded_frame, e->decoded_buffer);
}
return e;
}
static int dma_read_sync(MppBuffer buffer, bool start) {
dma_buf_sync sync{};
sync.flags = DMA_BUF_SYNC_READ | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END);
int ret;
do { ret = ioctl(mpp_buffer_get_fd(buffer), DMA_BUF_IOCTL_SYNC, &sync); }
while (ret < 0 && errno == EINTR);
return ret;
}
static int decode_jpeg(RkmppDmaEncoder *e, size_t index, size_t bytes_used) {
MppBuffer input = e->buffers[index];
// The parser reads only header bytes with explicit DMA CPU-read ownership.
if (dma_read_sync(input, true)) return fail("JPEG DMA read sync start", errno);
const auto *data = static_cast<const uint8_t *>(mpp_buffer_get_ptr(input));
const bool valid = rkmpp_dma_jpeg_header(data, bytes_used, e->width, e->height);
if (dma_read_sync(input, false)) return fail("JPEG DMA read sync end", errno);
if (!valid) return fail("unsupported/mismatched JPEG header", -1);
int ret = mpp_packet_init_with_buffer(&e->input_packet, input);
if (ret) return fail("JPEG input packet", ret);
mpp_packet_set_length(e->input_packet, bytes_used);
MppTask task = nullptr;
ret = e->dec_api->poll(e->decoder, MPP_PORT_INPUT, static_cast<MppPollType>(2000));
if (ret) return fail("JPEG input poll", ret);
ret = e->dec_api->dequeue(e->decoder, MPP_PORT_INPUT, &task);
if (ret || !task) return fail("JPEG input task", ret);
ret = mpp_task_meta_set_packet(task, KEY_INPUT_PACKET, e->input_packet);
if (ret) return fail("JPEG input metadata", ret);
ret = mpp_task_meta_set_frame(task, KEY_OUTPUT_FRAME, e->decoded_frame);
if (ret) return fail("JPEG output metadata", ret);
ret = e->dec_api->enqueue(e->decoder, MPP_PORT_INPUT, task);
if (ret) return fail("JPEG submit", ret);
task = nullptr;
ret = e->dec_api->poll(e->decoder, MPP_PORT_OUTPUT, static_cast<MppPollType>(2000));
if (ret) return fail("JPEG output poll", ret);
ret = e->dec_api->dequeue(e->decoder, MPP_PORT_OUTPUT, &task);
if (ret || !task) return fail("JPEG output task", ret);
MppFrame result = nullptr;
ret = mpp_task_meta_get_frame(task, KEY_OUTPUT_FRAME, &result);
if (ret || result != e->decoded_frame) return fail("JPEG output frame mismatch", ret);
ret = e->dec_api->enqueue(e->decoder, MPP_PORT_OUTPUT, task);
if (ret) return fail("JPEG return output task", ret);
ret = e->dec_api->poll(e->decoder, MPP_PORT_INPUT, static_cast<MppPollType>(2000));
if (ret) return fail("JPEG input completion", ret);
mpp_packet_deinit(&e->input_packet);
if (mpp_frame_get_errinfo(result) || mpp_frame_get_discard(result) ||
mpp_frame_get_info_change(result) ||
mpp_frame_get_width(result) != unsigned(e->width) ||
mpp_frame_get_height(result) != unsigned(e->height) ||
mpp_frame_get_fmt(result) != MPP_FMT_YUV420SP ||
mpp_frame_get_buffer(result) != e->decoded_buffer) {
std::snprintf(dma_error, sizeof(dma_error),
"invalid JPEG decoded frame: err=%u discard=%u info_change=%u size=%ux%u fmt=%x buffer_match=%d",
mpp_frame_get_errinfo(result), mpp_frame_get_discard(result), mpp_frame_get_info_change(result),
mpp_frame_get_width(result), mpp_frame_get_height(result), unsigned(mpp_frame_get_fmt(result)),
int(mpp_frame_get_buffer(result) == e->decoded_buffer));
return -1;
}
const int hs = mpp_frame_get_hor_stride(result), vs = mpp_frame_get_ver_stride(result);
if (hs < e->width || vs < e->height || hs > 8192 || vs > 8192 || (hs & 15) || (vs & 15) ||
size_t(hs) * vs * 3 / 2 > mpp_buffer_get_size(e->decoded_buffer))
return fail("invalid JPEG decoded stride", -1);
if (!e->decoded_layout_set) {
if (!set_cfg(e, "prep:hor_stride", hs) || !set_cfg(e, "prep:ver_stride", vs)) return -1;
ret = e->api->control(e->ctx, MPP_ENC_SET_CFG, e->cfg);
if (ret) return fail("JPEG encoder layout", ret);
e->decoded_stride = hs; e->decoded_vstride = vs; e->decoded_layout_set = true;
} else if (hs != e->decoded_stride || vs != e->decoded_vstride) {
return fail("JPEG decoded layout changed", -1);
}
return 0;
}
extern "C" int rkmpp_dma_encode(RkmppDmaEncoder *e, size_t index, size_t bytes_used, int fresh_fd, int64_t pts_us,
int force_idr, const uint8_t **data, size_t *size) {
if (!e || !e->ctx || index >= e->count || !data || !size)
return fail("invalid DMA encode call", -1);
*data = nullptr; *size = 0;
if (e->packet) mpp_packet_deinit(&e->packet);
auto abort_encode = [e](const char *op, int ret) {
fail(op, ret); e->close(); return -1;
};
if (bytes_used > e->capacities[index] ||
(e->jpeg ? (bytes_used < 4 || e->capacities[index] - bytes_used < 64) : bytes_used != e->minimum))
return abort_encode("invalid DMA payload length", -1);
if (fresh_fd >= 0) {
MppBuffer replacement = nullptr;
MppBufferInfo info{};
info.type = MPP_BUFFER_TYPE_EXT_DMA; info.fd = fresh_fd;
info.size = e->capacities[index]; info.index = static_cast<int>(index);
const int ret = mpp_buffer_import(&replacement, &info);
if (ret) return abort_encode("refresh USB DMA import", ret);
if (e->buffers[index]) mpp_buffer_put(e->buffers[index]);
e->buffers[index] = replacement;
}
if (e->jpeg && decode_jpeg(e, index, bytes_used)) {
// Preserve the detailed decoder failure while ending BOTH engines.
e->close(); return -1;
}
if (force_idr) {
int ret = e->api->control(e->ctx, MPP_ENC_SET_IDR_FRAME, nullptr);
if (ret) return abort_encode("MPP_ENC_SET_IDR_FRAME", ret);
}
MppFrame frame = e->jpeg ? e->decoded_frame : nullptr;
int ret = 0;
if (!e->jpeg) {
ret = mpp_frame_init(&frame);
if (ret) return abort_encode("mpp_frame_init", ret);
mpp_frame_set_width(frame, e->width); mpp_frame_set_height(frame, e->height);
mpp_frame_set_hor_stride(frame, e->stride); mpp_frame_set_ver_stride(frame, e->height);
mpp_frame_set_fmt(frame, e->format);
mpp_frame_set_buffer(frame, e->buffers[index]);
}
mpp_frame_set_pts(frame, pts_us);
ret = e->api->encode_put_frame(e->ctx, frame);
if (!e->jpeg) mpp_frame_deinit(&frame);
if (ret) return abort_encode("encode_put_frame", ret);
ret = e->api->encode_get_packet(e->ctx, &e->packet);
if (ret || !e->packet) return abort_encode("encode_get_packet", ret);
if (mpp_packet_is_partition(e->packet) || !mpp_packet_get_length(e->packet))
return abort_encode("incomplete DMA encoder output", -1);
// One synchronous input, no temporal scalability/reordering/split output.
// A completed packet is the input-consumption barrier for this mode.
*data = static_cast<const uint8_t *>(mpp_packet_get_pos(e->packet));
*size = mpp_packet_get_length(e->packet);
return 0;
}
extern "C" void rkmpp_dma_free(RkmppDmaEncoder *e) { delete e; }
#else
extern "C" RkmppDmaEncoder *rkmpp_dma_new(int,int,int,int,int,int,int,int,const int*,const size_t*,size_t) {
fail("RKMPP DMA support not built", -1); return nullptr;
}
extern "C" int rkmpp_dma_encode(RkmppDmaEncoder*,size_t,size_t,int,int64_t,int,const uint8_t**,size_t*) { return -1; }
extern "C" int rkmpp_dma_reconfigure(RkmppDmaEncoder*,int,int) { return -1; }
extern "C" void rkmpp_dma_free(RkmppDmaEncoder*) {}
#endif
extern "C" const char *rkmpp_dma_error(void) { return dma_error; }

View File

@@ -0,0 +1,31 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct RkmppDmaEncoder RkmppDmaEncoder;
// format: 0=NV12, 1=BGR24, 2=YUYV, 3=RGB24 (byte stride), 4=MJPEG (stride ignored).
// codec: 0=H264, 1=HEVC. MJPEG is decoded to hardware NV12, then encoded.
RkmppDmaEncoder *rkmpp_dma_new(int width, int height, int stride, int format,
int codec, int fps, int kbps, int gop,
const int *fds, const size_t *sizes, size_t count);
// Synchronous input-completion boundary. On failure the encoder is destroyed
// internally BEFORE returning, so it cannot keep reading the capture buffer.
// Output is borrowed until the next call or destruction; copy before reusing it.
// bytes_used must be the actual captured payload. MJPEG needs 64 bytes of
// readable allocation headroom; never pass sizeimage as the compressed length.
// fresh_fd=-1 reuses the original import (native HDMI). UVC supplies a new
// export of this dequeued slot. Keep it open until replacement/free; the old
// export can be closed AFTER this call, including on failure.
int rkmpp_dma_encode(RkmppDmaEncoder *encoder, size_t index, size_t bytes_used, int fresh_fd, int64_t pts_us,
int force_idr, const uint8_t **data, size_t *size);
int rkmpp_dma_reconfigure(RkmppDmaEncoder *encoder, int kbps, int gop);
void rkmpp_dma_free(RkmppDmaEncoder *encoder);
const char *rkmpp_dma_error(void);
#ifdef __cplusplus
}
#endif

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);
}
}