perf(video): 消除中间帧复制并优化 MJPEG 有界预取

通过 Bytes 所有权和只读 AVBufferRef 引用池化 NV12 输入,消除中间 AVFrame 的整帧复制,并保留 V4L2 编码包脱离硬件缓冲的处理。

允许输出健康时有界预取 MJPEG,在输出背压时优先解码前丢帧;增加等帧、输出等待、丢帧原因及编码分段耗时统计,提供预取和所有权输入的对照开关。

补充缓冲寿命、只读保护、软件编码回退和队列策略测试及接口说明,保留现有 PTS、解码 worker、音频和加密行为。
This commit is contained in:
mofeng-git
2026-10-01 20:29:06 +08:00
parent 318b58b26a
commit 2f60e61dd5
13 changed files with 994 additions and 41 deletions

View File

@@ -41,6 +41,22 @@ Based on the information above, there are several optimizations and changes made
* remove hevc_vaapi because of possible poor quality
* amf: not tested, https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/378
#### Owned NV12 input
The `bytes` feature provides `Encoder::encode_owned_bytes` for packed NV12
input to FFmpeg V4L2 M2M encoders. It holds a `Bytes` owner in a read-only
`AVBufferRef` until FFmpeg releases its last reference, instead of copying the
input into an intermediate AVFrame. Other encoders retain the existing copy
path. Converted or stride-compacted One-KVM input also retains that path.
`ONE_KVM_V4L2_OWNED_INPUT=0` disables this optimization when the encoder is
created, allowing comparisons with the same binary. `ONE_KVM_VIDEO_STATS=1`
enables preparation, send-frame, receive-packet, callback and output-copy
timings. One-KVM reports them in `[VideoInputPerf]` at five-second intervals;
`copied_bytes` counts only the intermediate raw-input copy, not FFmpeg's
remaining copy into V4L2 MMAP buffers. Encoded packet detachment remains in
place to avoid retaining scarce V4L2 capture buffers in downstream caches.
## System requirements
* intel

View File

@@ -5,6 +5,7 @@ extern "C" {
#include <libavutil/imgutils.h>
#include <libavutil/log.h>
#include <libavutil/opt.h>
#include <libavutil/time.h>
}
#include <stdbool.h>
@@ -14,6 +15,8 @@ extern "C" {
#include <string>
#include "common.h"
#include "ffmpeg_ram_encode_stats.h"
#include "owned_nv12_frame.h"
#define LOG_MODULE "FFMPEG_RAM_ENC"
#include <log.h>
@@ -146,6 +149,8 @@ public:
RamEncodePacketCallback packet_callback_ = NULL;
int offset_[AV_NUM_DATA_POINTERS] = {0};
bool force_keyframe_ = false; // Force next frame to be a keyframe
bool measure_ = false;
RamEncodeStats stats_ = {};
AVHWDeviceType hw_device_type_ = AV_HWDEVICE_TYPE_NONE;
AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE;
@@ -170,6 +175,8 @@ public:
thread_count_ = thread_count;
gpu_ = gpu;
callback_ = callback;
const char *stats_setting = getenv("ONE_KVM_VIDEO_STATS");
measure_ = stats_setting && strcmp(stats_setting, "1") == 0;
if (name_.find("vaapi") != std::string::npos) {
hw_device_type_ = AV_HWDEVICE_TYPE_VAAPI;
hw_pixfmt_ = AV_PIX_FMT_VAAPI;
@@ -298,6 +305,7 @@ public:
int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) {
int ret;
const int64_t prepare_start = timer_start();
if ((ret = av_frame_make_writable(frame_)) != 0) {
LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret));
@@ -316,9 +324,42 @@ public:
tmp_frame = frame_;
}
timer_add(stats_.prepare_us, prepare_start);
if (measure_) {
stats_.input_frames++;
stats_.copied_frames++;
const int bytes = av_image_get_buffer_size(pixfmt_, width_, height_, 1);
if (bytes > 0)
stats_.copied_bytes += bytes;
}
return do_encode(tmp_frame, obj, ms);
}
int encode_owned_packet(const uint8_t *data, int length, void *owner,
RamInputRelease release, const void *obj,
uint64_t ms, RamEncodePacketCallback callback) {
if (pixfmt_ != AV_PIX_FMT_NV12 || hw_device_type_ != AV_HWDEVICE_TYPE_NONE ||
name_.find("v4l2m2m") == std::string::npos) {
release(owner, const_cast<uint8_t *>(data));
return AVERROR(ENOSYS);
}
const int64_t prepare_start = timer_start();
auto frame = make_owned_nv12_frame(data, length, width_, height_, owner,
release);
timer_add(stats_.prepare_us, prepare_start);
if (!frame)
return AVERROR(EINVAL);
if (measure_) {
stats_.input_frames++;
stats_.borrowed_frames++;
}
packet_callback_ = callback;
int ret = do_encode(frame.get(), obj, ms);
packet_callback_ = NULL;
return ret;
}
int encode_packet(const uint8_t *data, int length, const void *obj,
uint64_t ms, RamEncodePacketCallback callback) {
packet_callback_ = callback;
@@ -345,6 +386,20 @@ public:
}
private:
int64_t timer_start() const { return measure_ ? av_gettime_relative() : 0; }
void timer_add(uint64_t &elapsed, int64_t start) const {
if (measure_)
elapsed += av_gettime_relative() - start;
}
int send_frame(AVFrame *frame) {
const int64_t start = timer_start();
const int ret = avcodec_send_frame(c_, frame);
timer_add(stats_.send_us, start);
return ret;
}
int set_hwframe_ctx() {
AVBufferRef *hw_frames_ref;
AVHWFramesContext *frames_ctx = NULL;
@@ -386,13 +441,13 @@ private:
}
force_keyframe_ = false;
ret = avcodec_send_frame(c_, frame);
ret = send_frame(frame);
if (ret == AVERROR(EAGAIN)) {
int drain_ret = receive_available_packets(obj, encoded);
if (drain_ret < 0) {
return drain_ret;
}
ret = avcodec_send_frame(c_, frame);
ret = send_frame(frame);
}
if (ret == AVERROR(EAGAIN)) {
return encoded ? 0 : AVERROR(EAGAIN);
@@ -417,7 +472,9 @@ private:
auto start = util::now();
while (util::elapsed_ms(start) < DECODE_TIMEOUT_MS) {
const int64_t receive_start = timer_start();
ret = avcodec_receive_packet(c_, pkt_);
timer_add(stats_.receive_us, receive_start);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
}
@@ -432,6 +489,7 @@ private:
continue;
}
encoded = true;
const int64_t packet_start = timer_start();
if (packet_callback_) {
AVPacket *owned_pkt = av_packet_clone(pkt_);
if (!owned_pkt) {
@@ -447,6 +505,7 @@ private:
pkt_->flags & AV_PKT_FLAG_KEY, obj);
}
av_packet_unref(pkt_);
timer_add(stats_.packet_us, packet_start);
}
return 0;
@@ -704,6 +763,31 @@ extern "C" void ffmpeg_ram_free_packet(void *packet) {
}
}
extern "C" int ffmpeg_ram_encode_owned_packet(
FFmpegRamEncoder *encoder, const uint8_t *data, int length, void *owner,
RamInputRelease release, const void *obj, uint64_t ms,
RamEncodePacketCallback callback) {
if (!encoder || !release) {
if (release)
release(owner, const_cast<uint8_t *>(data));
return AVERROR(EINVAL);
}
try {
return encoder->encode_owned_packet(data, length, owner, release, obj, ms,
callback);
} catch (const std::exception &e) {
encoder->packet_callback_ = NULL;
LOG_ERROR(std::string("encode_owned_packet failed, ") + e.what());
return -1;
}
}
extern "C" void ffmpeg_ram_encoder_stats(FFmpegRamEncoder *encoder,
RamEncodeStats *stats) {
if (encoder && stats)
*stats = encoder->stats_;
}
extern "C" int ffmpeg_ram_set_bitrate(FFmpegRamEncoder *encoder, int kbs) {
try {
return encoder->set_bitrate(kbs);

View File

@@ -0,0 +1,17 @@
#ifndef FFMPEG_RAM_ENCODE_STATS_H
#define FFMPEG_RAM_ENCODE_STATS_H
#include <stdint.h>
typedef struct RamEncodeStats {
uint64_t input_frames;
uint64_t borrowed_frames;
uint64_t copied_frames;
uint64_t copied_bytes;
uint64_t prepare_us;
uint64_t send_us;
uint64_t receive_us;
uint64_t packet_us;
} RamEncodeStats;
#endif

View File

@@ -2,6 +2,7 @@
#define FFMPEG_RAM_FFI_H
#include <stdint.h>
#include "ffmpeg_ram_encode_stats.h"
#define AV_NUM_DATA_POINTERS 8
@@ -24,6 +25,12 @@ int ffmpeg_ram_encode(void *encoder, const uint8_t *data, int length,
int ffmpeg_ram_encode_packet(void *encoder, const uint8_t *data, int length,
const void *obj, int64_t ms,
RamEncodePacketCallback callback);
int ffmpeg_ram_encode_owned_packet(void *encoder, const uint8_t *data,
int length, void *owner,
void (*release)(void *, uint8_t *),
const void *obj, int64_t ms,
RamEncodePacketCallback callback);
void ffmpeg_ram_encoder_stats(void *encoder, RamEncodeStats *stats);
void ffmpeg_ram_free_encoder(void *encoder);
void ffmpeg_ram_free_packet(void *packet);
int ffmpeg_ram_get_linesize_offset_length(int pix_fmt, int width, int height,

View File

@@ -0,0 +1,51 @@
#ifndef OWNED_NV12_FRAME_H
#define OWNED_NV12_FRAME_H
extern "C" {
#include <libavutil/frame.h>
}
#include <limits>
#include <memory>
using RamInputRelease = void (*)(void *, uint8_t *);
struct RamFrameDeleter {
void operator()(AVFrame *frame) const { av_frame_free(&frame); }
};
using RamOwnedFrame = std::unique_ptr<AVFrame, RamFrameDeleter>;
static RamOwnedFrame make_owned_nv12_frame(const uint8_t *data, int length,
int width, int height, void *owner,
RamInputRelease release) {
const int64_t pixels = static_cast<int64_t>(width) * height;
if (!data || !release || width <= 0 || height <= 0 || width % 2 ||
height % 2 || pixels > std::numeric_limits<int>::max() / 3 * 2 ||
length < pixels + pixels / 2) {
if (release)
release(owner, const_cast<uint8_t *>(data));
return RamOwnedFrame();
}
RamOwnedFrame frame(av_frame_alloc());
if (!frame) {
release(owner, const_cast<uint8_t *>(data));
return frame;
}
frame->buf[0] = av_buffer_create(const_cast<uint8_t *>(data), length, release,
owner, AV_BUFFER_FLAG_READONLY);
if (!frame->buf[0]) {
release(owner, const_cast<uint8_t *>(data));
return RamOwnedFrame();
}
frame->format = AV_PIX_FMT_NV12;
frame->width = width;
frame->height = height;
frame->data[0] = frame->buf[0]->data;
frame->data[1] = frame->data[0] + pixels;
frame->linesize[0] = width;
frame->linesize[1] = width;
return frame;
}
#endif

View File

@@ -1,5 +1,7 @@
#[cfg(feature = "bytes")]
use crate::ffmpeg_ram::{ffmpeg_ram_encode_packet, ffmpeg_ram_free_packet};
use crate::ffmpeg_ram::{
ffmpeg_ram_encode_owned_packet, ffmpeg_ram_encode_packet, ffmpeg_ram_free_packet,
};
use crate::{
common::DataFormat::{self, *},
ffmpeg::{init_av_log, AVPixelFormat},
@@ -498,6 +500,23 @@ pub struct Encoder {
pub linesize: Vec<i32>,
pub offset: Vec<i32>,
pub length: i32,
owned_nv12_input: bool,
measure: bool,
#[cfg(feature = "bytes")]
output_copy_us: u64,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct EncodeInputStats {
pub input_frames: u64,
pub borrowed_frames: u64,
pub copied_frames: u64,
pub copied_bytes: u64,
pub prepare_us: u64,
pub send_us: u64,
pub receive_us: u64,
pub packet_us: u64,
pub output_copy_us: u64,
}
impl Encoder {
@@ -545,6 +564,9 @@ impl Encoder {
return Err(());
}
let owned_nv12_input = ctx.name.ends_with("_v4l2m2m")
&& ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 as i32
&& std::env::var("ONE_KVM_V4L2_OWNED_INPUT").as_deref() != Ok("0");
Ok(Encoder {
codec,
frames: Box::into_raw(Box::new(Vec::<EncodeFrame>::new())),
@@ -552,6 +574,10 @@ impl Encoder {
linesize,
offset,
length: length[0],
owned_nv12_input,
measure: std::env::var("ONE_KVM_VIDEO_STATS").as_deref() == Ok("1"),
#[cfg(feature = "bytes")]
output_copy_us: 0,
})
}
}
@@ -581,33 +607,107 @@ impl Encoder {
#[cfg(feature = "bytes")]
pub fn encode_bytes(&mut self, data: &[u8], ms: i64) -> Result<Vec<EncodeBytesFrame>, i32> {
let length = c_int::try_from(data.len()).map_err(|_| -22)?;
unsafe {
let mut frames = Vec::<EncodeBytesFrame>::new();
let result = ffmpeg_ram_encode_packet(
self.codec,
data.as_ptr(),
data.len() as _,
length,
&mut frames as *mut _ as *const c_void,
ms,
Some(Encoder::packet_callback),
);
if result == -11 || result == 0 {
if self.ctx.name.contains("v4l2m2m") {
return Ok(frames
.into_iter()
.map(|frame| EncodeBytesFrame {
data: Bytes::copy_from_slice(frame.data.as_ref()),
pts: frame.pts,
key: frame.key,
})
.collect());
}
return Ok(frames);
}
Err(result)
self.finish_bytes_encode(frames, result)
}
}
pub fn supports_owned_nv12_input(&self) -> bool {
self.owned_nv12_input
}
pub fn input_stats(&self) -> Option<EncodeInputStats> {
if !self.measure {
return None;
}
let mut stats = unsafe { std::mem::zeroed() };
unsafe { crate::ffmpeg_ram::ffmpeg_ram_encoder_stats(self.codec, &mut stats) };
Some(EncodeInputStats {
input_frames: stats.input_frames,
borrowed_frames: stats.borrowed_frames,
copied_frames: stats.copied_frames,
copied_bytes: stats.copied_bytes,
prepare_us: stats.prepare_us,
send_us: stats.send_us,
receive_us: stats.receive_us,
packet_us: stats.packet_us,
#[cfg(feature = "bytes")]
output_copy_us: self.output_copy_us,
#[cfg(not(feature = "bytes"))]
output_copy_us: 0,
})
}
#[cfg(feature = "bytes")]
pub fn encode_owned_bytes(
&mut self,
data: Bytes,
ms: i64,
) -> Result<Vec<EncodeBytesFrame>, i32> {
if !self.owned_nv12_input {
return self.encode_bytes(data.as_ref(), ms);
}
let length = c_int::try_from(data.len()).map_err(|_| -22)?;
let owner = Box::new(data);
let pointer = owner.as_ptr();
let mut frames = Vec::<EncodeBytesFrame>::new();
let result = unsafe {
ffmpeg_ram_encode_owned_packet(
self.codec,
pointer,
length,
Box::into_raw(owner) as *mut c_void,
Some(Self::release_input),
&mut frames as *mut _ as *const c_void,
ms,
Some(Self::packet_callback),
)
};
self.finish_bytes_encode(frames, result)
}
#[cfg(feature = "bytes")]
fn finish_bytes_encode(
&mut self,
frames: Vec<EncodeBytesFrame>,
result: i32,
) -> Result<Vec<EncodeBytesFrame>, i32> {
if result != -11 && result != 0 {
return Err(result);
}
if !self.ctx.name.contains("v4l2m2m") {
return Ok(frames);
}
let start = self.measure.then(std::time::Instant::now);
let detached = frames
.into_iter()
.map(|frame| EncodeBytesFrame {
data: Bytes::copy_from_slice(frame.data.as_ref()),
pts: frame.pts,
key: frame.key,
})
.collect();
if let Some(start) = start {
self.output_copy_us += start.elapsed().as_micros() as u64;
}
Ok(detached)
}
#[cfg(feature = "bytes")]
extern "C" fn release_input(owner: *mut c_void, _data: *mut u8) {
unsafe { drop(Box::from_raw(owner as *mut Bytes)) };
}
extern "C" fn callback(data: *const u8, size: c_int, pts: i64, key: i32, obj: *const c_void) {
unsafe {
let frames = &mut *(obj as *mut Vec<EncodeFrame>);