perf(video): 改善视频卡顿问题并优化编码性能

改善内容:
1. NAL单元duration累积bug修复
   - 修改video_track.rs和unified_video_track.rs
   - 只有帧内最后一个NAL获得frame_duration,其他为ZERO
   - 确保同一帧的所有NAL共享相同的RTP时间戳

2. 修复VP8/VP9固定1秒duration错误
   - 将Duration::from_secs(1)改为正确的frame_duration计算

3. PTS计算优化(shared_video_pipeline.rs)
   - 将pipeline_start_time从Mutex<Option<Instant>>改为AtomicI64
   - 消除每帧一次的async mutex lock
   - 使用compare_exchange实现无锁的首帧时间设置

4. 避免重复读取config
   - 在encode_frame中缓存fps,避免末尾再次获取config锁

5. 编码器零拷贝优化
   - H264/H265/VP8/VP9编码器使用drain()替代clone()
   - 减少内存分配和拷贝开销

6. MJPEG处理器优化
   - 无客户端时跳过JPEG编码(WebRTC-only模式优化)

7. RKMPP YUYV直接输入支持
   - hwcodec C++层添加YUYV422格式支持
   - H264编码器添加Yuyv422输入格式选项
This commit is contained in:
mofeng-git
2026-01-02 11:58:55 +08:00
parent 0fc5be21c6
commit 04e62d1e3f
9 changed files with 168 additions and 40 deletions

View File

@@ -17,7 +17,7 @@
//! ```
use bytes::Bytes;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{broadcast, watch, Mutex, RwLock};
@@ -292,6 +292,8 @@ pub struct SharedVideoPipeline {
yuv420p_buffer: Mutex<Vec<u8>>,
/// Whether the encoder needs YUV420P (true) or NV12 (false)
encoder_needs_yuv420p: AtomicBool,
/// Whether YUYV direct input is enabled (RKMPP optimization)
yuyv_direct_input: AtomicBool,
frame_tx: broadcast::Sender<EncodedVideoFrame>,
stats: Mutex<SharedVideoPipelineStats>,
running: watch::Sender<bool>,
@@ -300,6 +302,9 @@ pub struct SharedVideoPipeline {
sequence: AtomicU64,
/// Atomic flag for keyframe request (avoids lock contention)
keyframe_requested: AtomicBool,
/// Pipeline start time for PTS calculation (epoch millis, 0 = not set)
/// Uses AtomicI64 instead of Mutex for lock-free access
pipeline_start_time_ms: AtomicI64,
}
impl SharedVideoPipeline {
@@ -329,12 +334,14 @@ impl SharedVideoPipeline {
nv12_buffer: Mutex::new(vec![0u8; nv12_size]),
yuv420p_buffer: Mutex::new(vec![0u8; yuv420p_size]),
encoder_needs_yuv420p: AtomicBool::new(false),
yuyv_direct_input: AtomicBool::new(false),
frame_tx,
stats: Mutex::new(SharedVideoPipelineStats::default()),
running: running_tx,
running_rx,
sequence: AtomicU64::new(0),
keyframe_requested: AtomicBool::new(false),
pipeline_start_time_ms: AtomicI64::new(0),
});
Ok(pipeline)
@@ -353,18 +360,41 @@ impl SharedVideoPipeline {
}
};
// Check if RKMPP backend is available for YUYV direct input optimization
let is_rkmpp_available = registry.encoder_with_backend(VideoEncoderType::H264, EncoderBackend::Rkmpp).is_some();
let use_yuyv_direct = is_rkmpp_available && config.input_format == PixelFormat::Yuyv;
if use_yuyv_direct {
info!("RKMPP backend detected with YUYV input, enabling YUYV direct input optimization");
}
// Create encoder based on codec type
let encoder: Box<dyn VideoEncoderTrait + Send> = match config.output_codec {
VideoEncoderType::H264 => {
// Determine H264 input format based on backend and input format
let h264_input_format = if use_yuyv_direct {
crate::video::encoder::h264::H264InputFormat::Yuyv422
} else {
crate::video::encoder::h264::H264InputFormat::Nv12
};
let encoder_config = H264Config {
base: EncoderConfig::h264(config.resolution, config.bitrate_kbps),
bitrate_kbps: config.bitrate_kbps,
gop_size: config.gop_size,
fps: config.fps,
input_format: crate::video::encoder::h264::H264InputFormat::Nv12,
input_format: h264_input_format,
};
let encoder = if let Some(ref backend) = config.encoder_backend {
let encoder = if use_yuyv_direct {
// Force RKMPP backend for YUYV direct input
let codec_name = get_codec_name(VideoEncoderType::H264, Some(EncoderBackend::Rkmpp))
.ok_or_else(|| AppError::VideoError(
"RKMPP backend not available for H.264".to_string()
))?;
info!("Creating H264 encoder with RKMPP backend for YUYV direct input (codec: {})", codec_name);
H264Encoder::with_codec(encoder_config, &codec_name)?
} else if let Some(ref backend) = config.encoder_backend {
// Specific backend requested
let codec_name = get_codec_name(VideoEncoderType::H264, Some(*backend))
.ok_or_else(|| AppError::VideoError(format!(
@@ -440,15 +470,19 @@ impl SharedVideoPipeline {
info!(
"Encoder {} needs {} format",
codec_name,
if needs_yuv420p { "YUV420P" } else { "NV12" }
if use_yuyv_direct { "YUYV422 (direct)" } else if needs_yuv420p { "YUV420P" } else { "NV12" }
);
// Create converter or decoder based on input format and encoder needs
info!("Initializing input format handler for: {} -> {}",
config.input_format,
if needs_yuv420p { "YUV420P" } else { "NV12" });
if use_yuyv_direct { "YUYV422 (direct)" } else if needs_yuv420p { "YUV420P" } else { "NV12" });
let (nv12_converter, yuv420p_converter, mjpeg_decoder, mjpeg_turbo_decoder) = if needs_yuv420p {
let (nv12_converter, yuv420p_converter, mjpeg_decoder, mjpeg_turbo_decoder) = if use_yuyv_direct {
// RKMPP with YUYV direct input - skip all conversion
info!("YUYV direct input enabled for RKMPP, skipping format conversion");
(None, None, None, None)
} else if needs_yuv420p {
// Software encoder needs YUV420P
match config.input_format {
PixelFormat::Yuv420 => {
@@ -527,6 +561,7 @@ impl SharedVideoPipeline {
*self.mjpeg_decoder.lock().await = mjpeg_decoder;
*self.mjpeg_turbo_decoder.lock().await = mjpeg_turbo_decoder;
self.encoder_needs_yuv420p.store(needs_yuv420p, Ordering::Release);
self.yuyv_direct_input.store(use_yuyv_direct, Ordering::Release);
Ok(())
}
@@ -749,7 +784,28 @@ impl SharedVideoPipeline {
let codec = config.output_codec;
drop(config);
let pts_ms = (frame_count * 1000 / fps as u64) as i64;
// Calculate PTS from real capture timestamp (lock-free using AtomicI64)
// This ensures smooth playback even when capture timing varies
let frame_ts_ms = frame.capture_ts.elapsed().as_millis() as i64;
// Convert Instant to a comparable value (negate elapsed to get "time since epoch")
let current_ts_ms = -(frame_ts_ms);
// Try to set start time if not yet set (first frame wins)
let start_ts = self.pipeline_start_time_ms.load(Ordering::Acquire);
let pts_ms = if start_ts == 0 {
// First frame - try to set the start time
// Use compare_exchange to ensure only one thread sets it
let _ = self.pipeline_start_time_ms.compare_exchange(
0,
current_ts_ms,
Ordering::AcqRel,
Ordering::Acquire,
);
0 // First frame has PTS 0
} else {
// Subsequent frames: PTS = current - start
current_ts_ms - start_ts
};
// Debug log for H265
if codec == VideoEncoderType::H265 && frame_count % 30 == 1 {
@@ -857,13 +913,12 @@ impl SharedVideoPipeline {
}
}
let config = self.config.read().await;
Ok(Some(EncodedVideoFrame {
data: Bytes::from(encoded.data),
pts_ms,
is_keyframe,
sequence,
duration: Duration::from_millis(1000 / config.fps as u64),
duration: Duration::from_millis(1000 / fps as u64),
codec,
}))
} else {