feat: 支持硬件编码能力测试,otg 自检修改为需要手动执行

This commit is contained in:
mofeng-git
2026-03-22 14:55:44 +08:00
parent c119db4908
commit 24a10aa222
8 changed files with 545 additions and 5 deletions

View File

@@ -17,6 +17,7 @@ pub mod h264;
pub mod h265;
pub mod jpeg;
pub mod registry;
pub mod self_check;
pub mod traits;
pub mod vp8;
pub mod vp9;
@@ -31,6 +32,10 @@ pub use codec::{CodecFrame, VideoCodec, VideoCodecConfig, VideoCodecFactory, Vid
// Encoder registry
pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncoderType};
pub use self_check::{
build_hardware_self_check_runtime_error, run_hardware_self_check, VideoEncoderSelfCheckCell,
VideoEncoderSelfCheckCodec, VideoEncoderSelfCheckResponse, VideoEncoderSelfCheckRow,
};
// H264 encoder
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};

View File

@@ -0,0 +1,335 @@
use serde::Serialize;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use super::{
EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder,
VP9Config, VP9Encoder, VideoEncoderType,
};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
const SELF_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
const SELF_CHECK_FRAME_ATTEMPTS: u64 = 3;
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckCodec {
pub id: &'static str,
pub name: &'static str,
}
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckCell {
pub codec_id: &'static str,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub elapsed_ms: Option<u64>,
}
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckRow {
pub resolution_id: &'static str,
pub resolution_label: &'static str,
pub width: u32,
pub height: u32,
pub cells: Vec<VideoEncoderSelfCheckCell>,
}
#[derive(Serialize)]
pub struct VideoEncoderSelfCheckResponse {
pub current_hardware_encoder: String,
pub codecs: Vec<VideoEncoderSelfCheckCodec>,
pub rows: Vec<VideoEncoderSelfCheckRow>,
}
pub fn run_hardware_self_check() -> VideoEncoderSelfCheckResponse {
let registry = EncoderRegistry::global();
let codecs = codec_columns();
let mut rows = Vec::new();
for (resolution_id, resolution_label, resolution) in test_resolutions() {
let mut cells = Vec::new();
for codec in test_codecs() {
let cell = match registry.best_encoder(codec, true) {
Some(encoder) => run_single_check(codec, resolution, encoder.codec_name.clone()),
None => unsupported_cell(codec),
};
cells.push(cell);
}
rows.push(VideoEncoderSelfCheckRow {
resolution_id,
resolution_label,
width: resolution.width,
height: resolution.height,
cells,
});
}
VideoEncoderSelfCheckResponse {
current_hardware_encoder: current_hardware_encoder(registry),
codecs,
rows,
}
}
pub fn build_hardware_self_check_runtime_error() -> VideoEncoderSelfCheckResponse {
let codecs = codec_columns();
let mut rows = Vec::new();
for (resolution_id, resolution_label, resolution) in test_resolutions() {
let cells = test_codecs()
.into_iter()
.map(|codec| VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: None,
})
.collect();
rows.push(VideoEncoderSelfCheckRow {
resolution_id,
resolution_label,
width: resolution.width,
height: resolution.height,
cells,
});
}
VideoEncoderSelfCheckResponse {
current_hardware_encoder: "None".to_string(),
codecs,
rows,
}
}
fn codec_columns() -> Vec<VideoEncoderSelfCheckCodec> {
test_codecs()
.into_iter()
.map(|codec| VideoEncoderSelfCheckCodec {
id: codec_id(codec),
name: match codec {
VideoEncoderType::H265 => "H.265",
_ => codec.display_name(),
},
})
.collect()
}
fn test_codecs() -> [VideoEncoderType; 4] {
[
VideoEncoderType::H264,
VideoEncoderType::H265,
VideoEncoderType::VP8,
VideoEncoderType::VP9,
]
}
fn test_resolutions() -> [(&'static str, &'static str, Resolution); 4] {
[
("720p", "720p", Resolution::HD720),
("1080p", "1080p", Resolution::HD1080),
("2k", "2K", Resolution::new(2560, 1440)),
("4k", "4K", Resolution::UHD4K),
]
}
fn codec_id(codec: VideoEncoderType) -> &'static str {
match codec {
VideoEncoderType::H264 => "h264",
VideoEncoderType::H265 => "h265",
VideoEncoderType::VP8 => "vp8",
VideoEncoderType::VP9 => "vp9",
}
}
fn unsupported_cell(codec: VideoEncoderType) -> VideoEncoderSelfCheckCell {
VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: None,
}
}
fn run_single_check(
codec: VideoEncoderType,
resolution: Resolution,
codec_name_ffmpeg: String,
) -> VideoEncoderSelfCheckCell {
let started = Instant::now();
let (tx, rx) = mpsc::channel();
let thread_codec_name = codec_name_ffmpeg.clone();
let spawn_result = std::thread::Builder::new()
.name(format!(
"encoder-self-check-{}-{}x{}",
codec_id(codec),
resolution.width,
resolution.height
))
.spawn(move || {
let _ = tx.send(run_smoke_test(codec, resolution, &thread_codec_name));
});
if let Err(e) = spawn_result {
let _ = e;
return VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
};
}
match rx.recv_timeout(SELF_CHECK_TIMEOUT) {
Ok(Ok(())) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: true,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
Ok(Err(_)) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
Err(mpsc::RecvTimeoutError::Timeout) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
Err(mpsc::RecvTimeoutError::Disconnected) => VideoEncoderSelfCheckCell {
codec_id: codec_id(codec),
ok: false,
elapsed_ms: Some(started.elapsed().as_millis() as u64),
},
}
}
fn current_hardware_encoder(registry: &EncoderRegistry) -> String {
let backends = registry
.available_backends()
.into_iter()
.filter(|backend| backend.is_hardware())
.map(|backend| backend.display_name().to_string())
.collect::<Vec<_>>();
if backends.is_empty() {
"None".to_string()
} else {
backends.join("/")
}
}
fn run_smoke_test(
codec: VideoEncoderType,
resolution: Resolution,
codec_name_ffmpeg: &str,
) -> Result<()> {
match codec {
VideoEncoderType::H264 => run_h264_smoke_test(resolution, codec_name_ffmpeg),
VideoEncoderType::H265 => run_h265_smoke_test(resolution, codec_name_ffmpeg),
VideoEncoderType::VP8 => run_vp8_smoke_test(resolution, codec_name_ffmpeg),
VideoEncoderType::VP9 => run_vp9_smoke_test(resolution, codec_name_ffmpeg),
}
}
fn run_h264_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = H264Encoder::with_codec(
H264Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
encoder.request_keyframe();
let frame = build_nv12_test_frame(resolution, encoder.yuv_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn run_h265_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = H265Encoder::with_codec(
H265Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
encoder.request_keyframe();
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn run_vp8_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = VP8Encoder::with_codec(
VP8Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn run_vp9_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
let mut encoder = VP9Encoder::with_codec(
VP9Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,
)?;
let frame = build_nv12_test_frame(resolution, encoder.buffer_info().2 as usize);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
))
}
fn build_nv12_test_frame(resolution: Resolution, buffer_length: usize) -> Vec<u8> {
let minimum_length = PixelFormat::Nv12.frame_size(resolution).unwrap_or(0);
let mut frame = vec![0x80; buffer_length.max(minimum_length)];
let y_plane_len = (resolution.width * resolution.height) as usize;
let fill_len = y_plane_len.min(frame.len());
frame[..fill_len].fill(0x10);
frame
}
fn bitrate_kbps_for_resolution(resolution: Resolution) -> u32 {
match resolution.width {
0..=1280 => 4_000,
1281..=1920 => 8_000,
1921..=2560 => 12_000,
_ => 20_000,
}
}
fn pts_ms(sequence: u64) -> i64 {
((sequence * 1000) / 30) as i64
}

View File

@@ -16,7 +16,10 @@ use crate::events::SystemEvent;
use crate::state::AppState;
use crate::update::{UpdateChannel, UpdateOverviewResponse, UpdateStatusResponse, UpgradeRequest};
use crate::video::codec_constraints::codec_to_id;
use crate::video::encoder::BitratePreset;
use crate::video::encoder::{
build_hardware_self_check_runtime_error, run_hardware_self_check, BitratePreset,
VideoEncoderSelfCheckResponse,
};
// ============================================================================
// Health & Info
@@ -1848,6 +1851,15 @@ pub async fn stream_codecs_list() -> Json<AvailableCodecsResponse> {
})
}
/// Run hardware encoder smoke tests across common resolutions/codecs.
pub async fn video_encoder_self_check() -> Json<VideoEncoderSelfCheckResponse> {
let response = tokio::task::spawn_blocking(run_hardware_self_check)
.await
.unwrap_or_else(|_| build_hardware_self_check_runtime_error());
Json(response)
}
/// Query parameters for MJPEG stream
#[derive(Deserialize, Default)]
pub struct MjpegStreamQuery {

View File

@@ -51,6 +51,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
.route("/stream/bitrate", post(handlers::stream_set_bitrate))
.route("/stream/codecs", get(handlers::stream_codecs_list))
.route("/stream/constraints", get(handlers::stream_constraints_get))
.route(
"/video/encoder/self-check",
get(handlers::video_encoder_self_check),
)
// WebRTC endpoints
.route("/webrtc/session", post(handlers::webrtc_create_session))
.route("/webrtc/offer", post(handlers::webrtc_offer))