mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
perf(video): 并行解码 MJPEG 并丢弃积压帧
按可用 CPU 数量启动独立 MJPEG 解码线程,通过无缓冲通道分发任务;线程全忙时丢弃新帧,按序列号拒绝过时解码结果。 复用帧缓冲池,在线程启动失败时回退到采集线程解码,并更新并行解码说明及工作线程数量测试。
This commit is contained in:
@@ -190,8 +190,8 @@ fn create_mjpeg_decoder(resolution: Resolution) -> Result<(MjpegDecoderKind, Pix
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// V4L2 M2M hardware encoding and libjpeg-turbo use independent CPU/hardware
|
/// V4L2 M2M hardware encoding and libjpeg-turbo use independent CPU/hardware
|
||||||
/// resources. Decode MJPEG in the capture worker so encoding the previous NV12
|
/// resources. Decode MJPEG outside the encoder worker so encoding the previous
|
||||||
/// frame can overlap with decoding the next frame.
|
/// NV12 frame can overlap with decoding subsequent frames.
|
||||||
pub(super) fn should_parallel_decode_mjpeg(config: &SharedVideoPipelineConfig) -> bool {
|
pub(super) fn should_parallel_decode_mjpeg(config: &SharedVideoPipelineConfig) -> bool {
|
||||||
if !config.input_format.is_compressed()
|
if !config.input_format.is_compressed()
|
||||||
|| !matches!(
|
|| !matches!(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use parking_lot::Mutex as ParkingMutex;
|
|||||||
use parking_lot::RwLock as ParkingRwLock;
|
use parking_lot::RwLock as ParkingRwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
|
||||||
|
use std::sync::mpsc::{sync_channel, SyncSender, TrySendError};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::{mpsc, watch, Mutex, RwLock};
|
use tokio::sync::{mpsc, watch, Mutex, RwLock};
|
||||||
@@ -60,6 +61,92 @@ use crate::video::recovery::{wait_for_source_change, CaptureRecoveryPolicy};
|
|||||||
use crate::video::signal::SignalStatus;
|
use crate::video::signal::SignalStatus;
|
||||||
|
|
||||||
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
|
||||||
|
struct MjpegDecodeJob {
|
||||||
|
data: Vec<u8>,
|
||||||
|
sequence: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mjpeg_decode_worker_count(available_parallelism: usize) -> usize {
|
||||||
|
available_parallelism.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_mjpeg_decode_workers(
|
||||||
|
pipeline: &Arc<SharedVideoPipeline>,
|
||||||
|
latest_frame: &Arc<ParkingRwLock<Option<Arc<VideoFrame>>>>,
|
||||||
|
frame_seq_tx: &watch::Sender<u64>,
|
||||||
|
buffer_pool: &Arc<FrameBufferPool>,
|
||||||
|
resolution: Resolution,
|
||||||
|
) -> Vec<SyncSender<MjpegDecodeJob>> {
|
||||||
|
let available = std::thread::available_parallelism()
|
||||||
|
.map(|count| count.get())
|
||||||
|
.unwrap_or(1);
|
||||||
|
let worker_count = mjpeg_decode_worker_count(available);
|
||||||
|
let mut senders = Vec::with_capacity(worker_count);
|
||||||
|
|
||||||
|
for worker_id in 0..worker_count {
|
||||||
|
// A rendezvous channel deliberately has no queue. If every decoder is
|
||||||
|
// busy, capture drops the new compressed frame instead of building up
|
||||||
|
// latency behind stale frames.
|
||||||
|
let (tx, rx) = sync_channel::<MjpegDecodeJob>(0);
|
||||||
|
let worker_pipeline = pipeline.clone();
|
||||||
|
let worker_latest_frame = latest_frame.clone();
|
||||||
|
let worker_frame_seq_tx = frame_seq_tx.clone();
|
||||||
|
let worker_buffer_pool = buffer_pool.clone();
|
||||||
|
let thread_name = format!("mjpeg-decoder-{worker_id}");
|
||||||
|
let spawn_result = std::thread::Builder::new()
|
||||||
|
.name(thread_name)
|
||||||
|
.spawn(move || {
|
||||||
|
let mut decoder = MjpegToNv12Decoder::new(resolution);
|
||||||
|
while let Ok(job) = rx.recv() {
|
||||||
|
let nv12_size = resolution.width as usize * resolution.height as usize * 3 / 2;
|
||||||
|
let mut nv12 = worker_buffer_pool.take(nv12_size);
|
||||||
|
let decode_result = decoder.decode_into(&job.data, &mut nv12);
|
||||||
|
worker_buffer_pool.put(job.data);
|
||||||
|
|
||||||
|
if let Err(error) = decode_result {
|
||||||
|
worker_buffer_pool.put(nv12);
|
||||||
|
warn!("Dropping undecodable MJPEG frame: {}", error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !worker_pipeline.running_flag.load(Ordering::Acquire) {
|
||||||
|
worker_buffer_pool.put(nv12);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = Arc::new(VideoFrame::from_pooled(
|
||||||
|
Arc::new(FrameBuffer::new(nv12, Some(worker_buffer_pool.clone()))),
|
||||||
|
resolution,
|
||||||
|
PixelFormat::Nv12,
|
||||||
|
resolution.width,
|
||||||
|
job.sequence,
|
||||||
|
));
|
||||||
|
let published = {
|
||||||
|
let mut latest = worker_latest_frame.write();
|
||||||
|
if latest
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|current| current.sequence >= job.sequence)
|
||||||
|
{
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
*latest = Some(frame);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if published {
|
||||||
|
let _ = worker_frame_seq_tx.send(job.sequence.wrapping_add(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match spawn_result {
|
||||||
|
Ok(_) => senders.push(tx),
|
||||||
|
Err(error) => error!("Failed to start MJPEG decoder worker: {}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Started {} parallel MJPEG decoder worker(s)", senders.len());
|
||||||
|
senders
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
|
||||||
use hwcodec::ffmpeg_hw::last_error_message as ffmpeg_hw_last_error;
|
use hwcodec::ffmpeg_hw::last_error_message as ffmpeg_hw_last_error;
|
||||||
@@ -738,7 +825,7 @@ impl SharedVideoPipeline {
|
|||||||
let mut encoder_config = config.clone();
|
let mut encoder_config = config.clone();
|
||||||
if parallel_mjpeg_decode {
|
if parallel_mjpeg_decode {
|
||||||
encoder_config.input_format = PixelFormat::Nv12;
|
encoder_config.input_format = PixelFormat::Nv12;
|
||||||
info!("Using capture-thread libyuv MJPEG decode with parallel hardware encoding");
|
info!("Using parallel libyuv MJPEG decode with hardware encoding");
|
||||||
}
|
}
|
||||||
let mut encoder_state = build_encoder_state(&encoder_config)?;
|
let mut encoder_state = build_encoder_state(&encoder_config)?;
|
||||||
let _ = self.running.send(true);
|
let _ = self.running.send(true);
|
||||||
@@ -864,8 +951,25 @@ impl SharedVideoPipeline {
|
|||||||
let mut pixel_format = config.input_format;
|
let mut pixel_format = config.input_format;
|
||||||
let mut active_fps = config.fps;
|
let mut active_fps = config.fps;
|
||||||
let mut stride: u32 = 0;
|
let mut stride: u32 = 0;
|
||||||
let mut mjpeg_decoder =
|
let mut mjpeg_decode_senders = parallel_mjpeg_decode
|
||||||
parallel_mjpeg_decode.then(|| MjpegToNv12Decoder::new(config.resolution));
|
.then(|| {
|
||||||
|
spawn_mjpeg_decode_workers(
|
||||||
|
&pipeline,
|
||||||
|
&latest_frame,
|
||||||
|
&frame_seq_tx,
|
||||||
|
&buffer_pool,
|
||||||
|
config.resolution,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|senders| !senders.is_empty());
|
||||||
|
let mut next_mjpeg_decoder = 0usize;
|
||||||
|
let mut mjpeg_decoder = parallel_mjpeg_decode
|
||||||
|
.then(|| MjpegToNv12Decoder::new(config.resolution))
|
||||||
|
.filter(|_| {
|
||||||
|
mjpeg_decode_senders
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|senders| senders.is_empty())
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(s) = preopened {
|
if let Some(s) = preopened {
|
||||||
resolution = s.resolution();
|
resolution = s.resolution();
|
||||||
@@ -1210,6 +1314,32 @@ impl SharedVideoPipeline {
|
|||||||
pixel_format,
|
pixel_format,
|
||||||
active_fps,
|
active_fps,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if let Some(senders) = mjpeg_decode_senders.as_mut() {
|
||||||
|
let mut pending = Some(MjpegDecodeJob {
|
||||||
|
data: owned,
|
||||||
|
sequence: meta.sequence,
|
||||||
|
});
|
||||||
|
for offset in 0..senders.len() {
|
||||||
|
let index = (next_mjpeg_decoder + offset) % senders.len();
|
||||||
|
let job = pending.take().expect("pending MJPEG decode job");
|
||||||
|
match senders[index].try_send(job) {
|
||||||
|
Ok(()) => {
|
||||||
|
next_mjpeg_decoder = (index + 1) % senders.len();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(TrySendError::Full(job))
|
||||||
|
| Err(TrySendError::Disconnected(job)) => {
|
||||||
|
pending = Some(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(job) = pending {
|
||||||
|
buffer_pool.put(job.data);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let (frame_data, frame_format, frame_stride) =
|
let (frame_data, frame_format, frame_stride) =
|
||||||
if let Some(decoder) = mjpeg_decoder.as_mut() {
|
if let Some(decoder) = mjpeg_decoder.as_mut() {
|
||||||
let nv12_size =
|
let nv12_size =
|
||||||
@@ -1832,6 +1962,14 @@ mod tests {
|
|||||||
assert!(Arc::ptr_eq(&received, &bootstrap));
|
assert!(Arc::ptr_eq(&received, &bootstrap));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mjpeg_workers_match_available_cpu_count() {
|
||||||
|
assert_eq!(mjpeg_decode_worker_count(1), 1);
|
||||||
|
assert_eq!(mjpeg_decode_worker_count(2), 2);
|
||||||
|
assert_eq!(mjpeg_decode_worker_count(4), 4);
|
||||||
|
assert_eq!(mjpeg_decode_worker_count(64), 64);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stop_request_does_not_publish_worker_exit() {
|
fn stop_request_does_not_publish_worker_exit() {
|
||||||
let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264(
|
let pipeline = SharedVideoPipeline::new(SharedVideoPipelineConfig::h264(
|
||||||
|
|||||||
Reference in New Issue
Block a user