From 0db287bf55fae58883a101676680b2fbdb16088f Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Sun, 22 Mar 2026 12:57:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20ffmpeg=20?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E5=99=A8=E6=8E=A2=E6=B5=8B=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/hwcodec/src/ffmpeg_ram/encode.rs | 696 +++++++++++++++----------- libs/hwcodec/src/ffmpeg_ram/mod.rs | 83 +-- src/rustdesk/connection.rs | 18 +- src/video/encoder/h264.rs | 50 +- src/video/encoder/h265.rs | 43 +- src/video/encoder/mod.rs | 46 +- src/video/encoder/registry.rs | 193 +++---- src/video/encoder/vp8.rs | 33 +- src/video/encoder/vp9.rs | 33 +- src/video/shared_video_pipeline.rs | 29 +- src/video/video_session.rs | 7 +- src/web/handlers/mod.rs | 8 +- src/webrtc/webrtc_streamer.rs | 33 +- 13 files changed, 704 insertions(+), 568 deletions(-) diff --git a/libs/hwcodec/src/ffmpeg_ram/encode.rs b/libs/hwcodec/src/ffmpeg_ram/encode.rs index 263b04fc..00cef421 100644 --- a/libs/hwcodec/src/ffmpeg_ram/encode.rs +++ b/libs/hwcodec/src/ffmpeg_ram/encode.rs @@ -15,12 +15,412 @@ use std::{ slice, }; -use super::Priority; #[cfg(any(windows, target_os = "linux"))] use crate::common::Driver; /// Timeout for encoder test in milliseconds const TEST_TIMEOUT_MS: u64 = 3000; +const PRIORITY_NVENC: i32 = 0; +const PRIORITY_QSV: i32 = 1; +const PRIORITY_AMF: i32 = 2; +const PRIORITY_RKMPP: i32 = 3; +const PRIORITY_VAAPI: i32 = 4; +const PRIORITY_V4L2M2M: i32 = 5; + +#[derive(Clone, Copy)] +struct CandidateCodecSpec { + name: &'static str, + format: DataFormat, + priority: i32, +} + +fn push_candidate(codecs: &mut Vec, candidate: CandidateCodecSpec) { + codecs.push(CodecInfo { + name: candidate.name.to_owned(), + format: candidate.format, + priority: candidate.priority, + ..Default::default() + }); +} + +#[cfg(target_os = "linux")] +fn linux_support_vaapi() -> bool { + let entries = match std::fs::read_dir("/dev/dri") { + Ok(entries) => entries, + Err(_) => return false, + }; + + entries.flatten().any(|entry| { + entry + .file_name() + .to_str() + .map(|name| name.starts_with("renderD")) + .unwrap_or(false) + }) +} + +#[cfg(not(target_os = "linux"))] +fn linux_support_vaapi() -> bool { + false +} + +#[cfg(target_os = "linux")] +fn linux_support_rkmpp() -> bool { + extern "C" { + fn linux_support_rkmpp() -> c_int; + } + + unsafe { linux_support_rkmpp() == 0 } +} + +#[cfg(not(target_os = "linux"))] +fn linux_support_rkmpp() -> bool { + false +} + +#[cfg(target_os = "linux")] +fn linux_support_v4l2m2m() -> bool { + extern "C" { + fn linux_support_v4l2m2m() -> c_int; + } + + unsafe { linux_support_v4l2m2m() == 0 } +} + +#[cfg(not(target_os = "linux"))] +fn linux_support_v4l2m2m() -> bool { + false +} + +#[cfg(any(windows, target_os = "linux"))] +fn enumerate_candidate_codecs(ctx: &EncodeContext) -> Vec { + use log::debug; + + let mut codecs = Vec::new(); + let contains = |_vendor: Driver, _format: DataFormat| { + // Without VRAM feature, we can't check SDK availability. + // Keep the prefilter coarse and let FFmpeg validation do the real check. + true + }; + let (nv, amf, intel) = crate::common::supported_gpu(true); + + debug!( + "GPU support detected - NV: {}, AMF: {}, Intel: {}", + nv, amf, intel + ); + + if nv && contains(Driver::NV, H264) { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "h264_nvenc", + format: H264, + priority: PRIORITY_NVENC, + }, + ); + } + if nv && contains(Driver::NV, H265) { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "hevc_nvenc", + format: H265, + priority: PRIORITY_NVENC, + }, + ); + } + if intel && contains(Driver::MFX, H264) { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "h264_qsv", + format: H264, + priority: PRIORITY_QSV, + }, + ); + } + if intel && contains(Driver::MFX, H265) { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "hevc_qsv", + format: H265, + priority: PRIORITY_QSV, + }, + ); + } + if amf && contains(Driver::AMF, H264) { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "h264_amf", + format: H264, + priority: PRIORITY_AMF, + }, + ); + } + if amf && contains(Driver::AMF, H265) { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "hevc_amf", + format: H265, + priority: PRIORITY_AMF, + }, + ); + } + if linux_support_rkmpp() { + debug!("RKMPP hardware detected, adding Rockchip encoders"); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "h264_rkmpp", + format: H264, + priority: PRIORITY_RKMPP, + }, + ); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "hevc_rkmpp", + format: H265, + priority: PRIORITY_RKMPP, + }, + ); + } + if cfg!(target_os = "linux") && linux_support_vaapi() { + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "h264_vaapi", + format: H264, + priority: PRIORITY_VAAPI, + }, + ); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "hevc_vaapi", + format: H265, + priority: PRIORITY_VAAPI, + }, + ); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "vp8_vaapi", + format: VP8, + priority: PRIORITY_VAAPI, + }, + ); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "vp9_vaapi", + format: VP9, + priority: PRIORITY_VAAPI, + }, + ); + } + if linux_support_v4l2m2m() { + debug!("V4L2 M2M hardware detected, adding V4L2 encoders"); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "h264_v4l2m2m", + format: H264, + priority: PRIORITY_V4L2M2M, + }, + ); + push_candidate( + &mut codecs, + CandidateCodecSpec { + name: "hevc_v4l2m2m", + format: H265, + priority: PRIORITY_V4L2M2M, + }, + ); + } + + codecs.retain(|codec| { + !(ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P && codec.name.contains("qsv")) + }); + codecs +} + +#[derive(Clone, Copy)] +struct ProbePolicy { + max_attempts: usize, + request_keyframe: bool, + accept_any_output: bool, +} + +impl ProbePolicy { + fn for_codec(codec_name: &str) -> Self { + if codec_name.contains("v4l2m2m") { + Self { + max_attempts: 5, + request_keyframe: true, + accept_any_output: true, + } + } else { + Self { + max_attempts: 1, + request_keyframe: false, + accept_any_output: false, + } + } + } + + fn prepare_attempt(&self, encoder: &mut Encoder) { + if self.request_keyframe { + encoder.request_keyframe(); + } + } + + fn passed(&self, frames: &[EncodeFrame], elapsed_ms: u128) -> bool { + if elapsed_ms >= TEST_TIMEOUT_MS as u128 { + return false; + } + + if self.accept_any_output { + !frames.is_empty() + } else { + frames.len() == 1 && frames[0].key == 1 + } + } +} + +fn log_failed_probe_attempt( + codec_name: &str, + policy: ProbePolicy, + attempt: usize, + frames: &[EncodeFrame], + elapsed_ms: u128, +) { + use log::debug; + + if policy.accept_any_output { + if frames.is_empty() { + debug!( + "Encoder {} test produced no output on attempt {}", + codec_name, attempt + ); + } else { + debug!( + "Encoder {} test failed on attempt {} - frames: {}, timeout: {}ms", + codec_name, + attempt, + frames.len(), + elapsed_ms + ); + } + } else if frames.len() == 1 { + debug!( + "Encoder {} test failed on attempt {} - key: {}, timeout: {}ms", + codec_name, attempt, frames[0].key, elapsed_ms + ); + } else { + debug!( + "Encoder {} test failed on attempt {} - wrong frame count: {}", + codec_name, + attempt, + frames.len() + ); + } +} + +fn validate_candidate(codec: &CodecInfo, ctx: &EncodeContext, yuv: &[u8]) -> bool { + use log::debug; + + debug!("Testing encoder: {}", codec.name); + + let test_ctx = EncodeContext { + name: codec.name.clone(), + mc_name: codec.mc_name.clone(), + ..ctx.clone() + }; + + match Encoder::new(test_ctx) { + Ok(mut encoder) => { + debug!("Encoder {} created successfully", codec.name); + let policy = ProbePolicy::for_codec(&codec.name); + let mut last_err: Option = None; + + for attempt in 0..policy.max_attempts { + let attempt_no = attempt + 1; + policy.prepare_attempt(&mut encoder); + + let pts = (attempt as i64) * 33; + let start = std::time::Instant::now(); + match encoder.encode(yuv, pts) { + Ok(frames) => { + let elapsed = start.elapsed().as_millis(); + + if policy.passed(frames, elapsed) { + if policy.accept_any_output { + debug!( + "Encoder {} test passed on attempt {} (frames: {})", + codec.name, + attempt_no, + frames.len() + ); + } else { + debug!( + "Encoder {} test passed on attempt {}", + codec.name, attempt_no + ); + } + return true; + } else { + log_failed_probe_attempt( + &codec.name, + policy, + attempt_no, + frames, + elapsed, + ); + } + } + Err(err) => { + last_err = Some(err); + debug!( + "Encoder {} test attempt {} returned error: {}", + codec.name, attempt_no, err + ); + } + } + } + + debug!( + "Encoder {} test failed after retries{}", + codec.name, + last_err + .map(|e| format!(" (last err: {})", e)) + .unwrap_or_default() + ); + false + } + Err(_) => { + debug!("Failed to create encoder {}", codec.name); + false + } + } +} + +fn add_software_fallback(codecs: &mut Vec) { + use log::debug; + + for fallback in CodecInfo::soft().into_vec() { + if !codecs.iter().any(|codec| codec.format == fallback.format) { + debug!( + "Adding software {:?} encoder: {}", + fallback.format, fallback.name + ); + codecs.push(fallback); + } + } +} #[derive(Debug, Clone, PartialEq)] pub struct EncodeContext { @@ -185,305 +585,21 @@ impl Encoder { if !(cfg!(windows) || cfg!(target_os = "linux")) { return vec![]; } - let mut codecs: Vec = vec![]; - #[cfg(any(windows, target_os = "linux"))] - { - let contains = |_vendor: Driver, _format: DataFormat| { - // Without VRAM feature, we can't check SDK availability - // Just return true and let FFmpeg handle the actual detection - true - }; - let (_nv, amf, _intel) = crate::common::supported_gpu(true); - debug!( - "GPU support detected - NV: {}, AMF: {}, Intel: {}", - _nv, amf, _intel - ); - - #[cfg(windows)] - if _intel && contains(Driver::MFX, H264) { - codecs.push(CodecInfo { - name: "h264_qsv".to_owned(), - format: H264, - priority: Priority::Best as _, - ..Default::default() - }); - } - #[cfg(windows)] - if _intel && contains(Driver::MFX, H265) { - codecs.push(CodecInfo { - name: "hevc_qsv".to_owned(), - format: H265, - priority: Priority::Best as _, - ..Default::default() - }); - } - if _nv && contains(Driver::NV, H264) { - codecs.push(CodecInfo { - name: "h264_nvenc".to_owned(), - format: H264, - priority: Priority::Best as _, - ..Default::default() - }); - } - if _nv && contains(Driver::NV, H265) { - codecs.push(CodecInfo { - name: "hevc_nvenc".to_owned(), - format: H265, - priority: Priority::Best as _, - ..Default::default() - }); - } - if amf && contains(Driver::AMF, H264) { - codecs.push(CodecInfo { - name: "h264_amf".to_owned(), - format: H264, - priority: Priority::Best as _, - ..Default::default() - }); - } - if amf { - codecs.push(CodecInfo { - name: "hevc_amf".to_owned(), - format: H265, - priority: Priority::Best as _, - ..Default::default() - }); - } - #[cfg(target_os = "linux")] - { - codecs.push(CodecInfo { - name: "h264_vaapi".to_owned(), - format: H264, - priority: Priority::Good as _, - ..Default::default() - }); - codecs.push(CodecInfo { - name: "hevc_vaapi".to_owned(), - format: H265, - priority: Priority::Good as _, - ..Default::default() - }); - codecs.push(CodecInfo { - name: "vp8_vaapi".to_owned(), - format: VP8, - priority: Priority::Good as _, - ..Default::default() - }); - codecs.push(CodecInfo { - name: "vp9_vaapi".to_owned(), - format: VP9, - priority: Priority::Good as _, - ..Default::default() - }); - - // Rockchip MPP hardware encoder support - use std::ffi::c_int; - extern "C" { - fn linux_support_rkmpp() -> c_int; - fn linux_support_v4l2m2m() -> c_int; - } - - if unsafe { linux_support_rkmpp() } == 0 { - debug!("RKMPP hardware detected, adding Rockchip encoders"); - codecs.push(CodecInfo { - name: "h264_rkmpp".to_owned(), - format: H264, - priority: Priority::Best as _, - ..Default::default() - }); - codecs.push(CodecInfo { - name: "hevc_rkmpp".to_owned(), - format: H265, - priority: Priority::Best as _, - ..Default::default() - }); - } - - // V4L2 Memory-to-Memory hardware encoder support (generic ARM) - if unsafe { linux_support_v4l2m2m() } == 0 { - debug!("V4L2 M2M hardware detected, adding V4L2 encoders"); - codecs.push(CodecInfo { - name: "h264_v4l2m2m".to_owned(), - format: H264, - priority: Priority::Good as _, - ..Default::default() - }); - codecs.push(CodecInfo { - name: "hevc_v4l2m2m".to_owned(), - format: H265, - priority: Priority::Good as _, - ..Default::default() - }); - } - } - } - - // qsv doesn't support yuv420p - codecs.retain(|c| { - let ctx = ctx.clone(); - if ctx.pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P && c.name.contains("qsv") { - return false; - } - return true; - }); - let mut res = vec![]; + #[cfg(any(windows, target_os = "linux"))] + let codecs = enumerate_candidate_codecs(&ctx); if let Ok(yuv) = Encoder::dummy_yuv(ctx.clone()) { for codec in codecs { - // Skip if this format already exists in results - if res - .iter() - .any(|existing: &CodecInfo| existing.format == codec.format) - { - continue; - } - - debug!("Testing encoder: {}", codec.name); - - let c = EncodeContext { - name: codec.name.clone(), - mc_name: codec.mc_name.clone(), - ..ctx - }; - - match Encoder::new(c) { - Ok(mut encoder) => { - debug!("Encoder {} created successfully", codec.name); - let mut passed = false; - let mut last_err: Option = None; - let is_v4l2m2m = codec.name.contains("v4l2m2m"); - - let max_attempts = if is_v4l2m2m { 5 } else { 1 }; - for attempt in 0..max_attempts { - if is_v4l2m2m { - encoder.request_keyframe(); - } - let pts = (attempt as i64) * 33; // 33ms is an approximation for 30 FPS (1000 / 30) - let start = std::time::Instant::now(); - match encoder.encode(&yuv, pts) { - Ok(frames) => { - let elapsed = start.elapsed().as_millis(); - - if is_v4l2m2m { - if !frames.is_empty() && elapsed < TEST_TIMEOUT_MS as _ { - debug!( - "Encoder {} test passed on attempt {} (frames: {})", - codec.name, - attempt + 1, - frames.len() - ); - res.push(codec.clone()); - passed = true; - break; - } else if frames.is_empty() { - debug!( - "Encoder {} test produced no output on attempt {}", - codec.name, - attempt + 1 - ); - } else { - debug!( - "Encoder {} test failed on attempt {} - frames: {}, timeout: {}ms", - codec.name, - attempt + 1, - frames.len(), - elapsed - ); - } - } else if frames.len() == 1 { - if frames[0].key == 1 && elapsed < TEST_TIMEOUT_MS as _ { - debug!( - "Encoder {} test passed on attempt {}", - codec.name, - attempt + 1 - ); - res.push(codec.clone()); - passed = true; - break; - } else { - debug!( - "Encoder {} test failed on attempt {} - key: {}, timeout: {}ms", - codec.name, - attempt + 1, - frames[0].key, - elapsed - ); - } - } else { - debug!( - "Encoder {} test failed on attempt {} - wrong frame count: {}", - codec.name, - attempt + 1, - frames.len() - ); - } - } - Err(err) => { - last_err = Some(err); - debug!( - "Encoder {} test attempt {} returned error: {}", - codec.name, - attempt + 1, - err - ); - } - } - } - - if !passed { - debug!( - "Encoder {} test failed after retries{}", - codec.name, - last_err - .map(|e| format!(" (last err: {})", e)) - .unwrap_or_default() - ); - } - } - Err(_) => { - debug!("Failed to create encoder {}", codec.name); - } + if validate_candidate(&codec, &ctx, &yuv) { + res.push(codec); } } } else { debug!("Failed to generate dummy YUV data"); } - // Add software encoders as fallback - let soft_codecs = CodecInfo::soft(); - - // Add H264 software encoder if not already present - if !res.iter().any(|c| c.format == H264) { - if let Some(h264_soft) = soft_codecs.h264 { - debug!("Adding software H264 encoder: {}", h264_soft.name); - res.push(h264_soft); - } - } - - // Add H265 software encoder if not already present - if !res.iter().any(|c| c.format == H265) { - if let Some(h265_soft) = soft_codecs.h265 { - debug!("Adding software H265 encoder: {}", h265_soft.name); - res.push(h265_soft); - } - } - - // Add VP8 software encoder if not already present - if !res.iter().any(|c| c.format == VP8) { - if let Some(vp8_soft) = soft_codecs.vp8 { - debug!("Adding software VP8 encoder: {}", vp8_soft.name); - res.push(vp8_soft); - } - } - - // Add VP9 software encoder if not already present - if !res.iter().any(|c| c.format == VP9) { - if let Some(vp9_soft) = soft_codecs.vp9 { - debug!("Adding software VP9 encoder: {}", vp9_soft.name); - res.push(vp9_soft); - } - } + add_software_fallback(&mut res); res } diff --git a/libs/hwcodec/src/ffmpeg_ram/mod.rs b/libs/hwcodec/src/ffmpeg_ram/mod.rs index 2ec0f758..be4abccd 100644 --- a/libs/hwcodec/src/ffmpeg_ram/mod.rs +++ b/libs/hwcodec/src/ffmpeg_ram/mod.rs @@ -86,6 +86,40 @@ impl Default for CodecInfo { } impl CodecInfo { + pub fn software(format: DataFormat) -> Option { + match format { + H264 => Some(CodecInfo { + name: "libx264".to_owned(), + mc_name: Default::default(), + format: H264, + hwdevice: AV_HWDEVICE_TYPE_NONE, + priority: Priority::Soft as _, + }), + H265 => Some(CodecInfo { + name: "libx265".to_owned(), + mc_name: Default::default(), + format: H265, + hwdevice: AV_HWDEVICE_TYPE_NONE, + priority: Priority::Soft as _, + }), + VP8 => Some(CodecInfo { + name: "libvpx".to_owned(), + mc_name: Default::default(), + format: VP8, + hwdevice: AV_HWDEVICE_TYPE_NONE, + priority: Priority::Soft as _, + }), + VP9 => Some(CodecInfo { + name: "libvpx-vp9".to_owned(), + mc_name: Default::default(), + format: VP9, + hwdevice: AV_HWDEVICE_TYPE_NONE, + priority: Priority::Soft as _, + }), + AV1 => None, + } + } + pub fn prioritized(coders: Vec) -> CodecInfos { let mut h264: Option = None; let mut h265: Option = None; @@ -148,34 +182,10 @@ impl CodecInfo { pub fn soft() -> CodecInfos { CodecInfos { - h264: Some(CodecInfo { - name: "libx264".to_owned(), - mc_name: Default::default(), - format: H264, - hwdevice: AV_HWDEVICE_TYPE_NONE, - priority: Priority::Soft as _, - }), - h265: Some(CodecInfo { - name: "libx265".to_owned(), - mc_name: Default::default(), - format: H265, - hwdevice: AV_HWDEVICE_TYPE_NONE, - priority: Priority::Soft as _, - }), - vp8: Some(CodecInfo { - name: "libvpx".to_owned(), - mc_name: Default::default(), - format: VP8, - hwdevice: AV_HWDEVICE_TYPE_NONE, - priority: Priority::Soft as _, - }), - vp9: Some(CodecInfo { - name: "libvpx-vp9".to_owned(), - mc_name: Default::default(), - format: VP9, - hwdevice: AV_HWDEVICE_TYPE_NONE, - priority: Priority::Soft as _, - }), + h264: CodecInfo::software(H264), + h265: CodecInfo::software(H265), + vp8: CodecInfo::software(VP8), + vp9: CodecInfo::software(VP9), av1: None, } } @@ -191,6 +201,23 @@ pub struct CodecInfos { } impl CodecInfos { + pub fn into_vec(self) -> Vec { + let mut codecs = Vec::new(); + if let Some(codec) = self.h264 { + codecs.push(codec); + } + if let Some(codec) = self.h265 { + codecs.push(codec); + } + if let Some(codec) = self.vp8 { + codecs.push(codec); + } + if let Some(codec) = self.vp9 { + codecs.push(codec); + } + codecs + } + pub fn serialize(&self) -> Result { match serde_json::to_string_pretty(self) { Ok(s) => Ok(s), diff --git a/src/rustdesk/connection.rs b/src/rustdesk/connection.rs index 83b41f36..c2437012 100644 --- a/src/rustdesk/connection.rs +++ b/src/rustdesk/connection.rs @@ -652,22 +652,22 @@ impl Connection { // H264 is preferred because it has the best hardware encoder support (RKMPP, VAAPI, etc.) // and most RustDesk clients support H264 hardware decoding if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H264) - && registry.is_format_available(VideoEncoderType::H264, false) + && registry.is_codec_available(VideoEncoderType::H264) { return VideoEncoderType::H264; } if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H265) - && registry.is_format_available(VideoEncoderType::H265, false) + && registry.is_codec_available(VideoEncoderType::H265) { return VideoEncoderType::H265; } if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP8) - && registry.is_format_available(VideoEncoderType::VP8, false) + && registry.is_codec_available(VideoEncoderType::VP8) { return VideoEncoderType::VP8; } if constraints.is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP9) - && registry.is_format_available(VideoEncoderType::VP9, false) + && registry.is_codec_available(VideoEncoderType::VP9) { return VideoEncoderType::VP9; } @@ -784,7 +784,7 @@ impl Connection { } let registry = EncoderRegistry::global(); - if registry.is_format_available(new_codec, false) { + if registry.is_codec_available(new_codec) { info!( "Client requested codec switch: {:?} -> {:?}", self.negotiated_codec, new_codec @@ -1121,16 +1121,16 @@ impl Connection { // Check which encoders are available (include software fallback) let h264_available = constraints .is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H264) - && registry.is_format_available(VideoEncoderType::H264, false); + && registry.is_codec_available(VideoEncoderType::H264); let h265_available = constraints .is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::H265) - && registry.is_format_available(VideoEncoderType::H265, false); + && registry.is_codec_available(VideoEncoderType::H265); let vp8_available = constraints .is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP8) - && registry.is_format_available(VideoEncoderType::VP8, false); + && registry.is_codec_available(VideoEncoderType::VP8); let vp9_available = constraints .is_webrtc_codec_allowed(crate::video::encoder::VideoCodecType::VP9) - && registry.is_format_available(VideoEncoderType::VP9, false); + && registry.is_codec_available(VideoEncoderType::VP9); info!( "Server encoding capabilities: H264={}, H265={}, VP8={}, VP9={}", diff --git a/src/video/encoder/h264.rs b/src/video/encoder/h264.rs index b7fdce24..d72ef49a 100644 --- a/src/video/encoder/h264.rs +++ b/src/video/encoder/h264.rs @@ -17,6 +17,8 @@ use hwcodec::ffmpeg::AVPixelFormat; use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder}; use hwcodec::ffmpeg_ram::CodecInfo; +use super::detect_best_codec_for_format; +use super::registry::EncoderBackend; use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig}; use crate::error::{AppError, Result}; use crate::video::format::{PixelFormat, Resolution}; @@ -69,21 +71,17 @@ impl std::fmt::Display for H264EncoderType { } /// Map codec name to encoder type -fn codec_name_to_type(name: &str) -> H264EncoderType { - if name.contains("nvenc") { - H264EncoderType::Nvenc - } else if name.contains("qsv") { - H264EncoderType::Qsv - } else if name.contains("amf") { - H264EncoderType::Amf - } else if name.contains("vaapi") { - H264EncoderType::Vaapi - } else if name.contains("rkmpp") { - H264EncoderType::Rkmpp - } else if name.contains("v4l2m2m") { - H264EncoderType::V4l2M2m - } else { - H264EncoderType::Software +impl From for H264EncoderType { + fn from(backend: EncoderBackend) -> Self { + match backend { + EncoderBackend::Nvenc => H264EncoderType::Nvenc, + EncoderBackend::Qsv => H264EncoderType::Qsv, + EncoderBackend::Amf => H264EncoderType::Amf, + EncoderBackend::Vaapi => H264EncoderType::Vaapi, + EncoderBackend::Rkmpp => H264EncoderType::Rkmpp, + EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m, + EncoderBackend::Software => H264EncoderType::Software, + } } } @@ -215,21 +213,15 @@ pub fn get_available_encoders(width: u32, height: u32) -> Vec { pub fn detect_best_encoder(width: u32, height: u32) -> (H264EncoderType, Option) { let encoders = get_available_encoders(width, height); - if encoders.is_empty() { + if let Some((encoder_type, codec_name)) = + detect_best_codec_for_format(&encoders, hwcodec::common::DataFormat::H264, |_| true) + { + info!("Best H.264 encoder: {} ({})", codec_name, encoder_type); + (encoder_type, Some(codec_name)) + } else { warn!("No H.264 encoders available from hwcodec"); - return (H264EncoderType::None, None); + (H264EncoderType::None, None) } - - // Find H264 encoder (not H265) - for codec in &encoders { - if codec.format == hwcodec::common::DataFormat::H264 { - let encoder_type = codec_name_to_type(&codec.name); - info!("Best H.264 encoder: {} ({})", codec.name, encoder_type); - return (encoder_type, Some(codec.name.clone())); - } - } - - (H264EncoderType::None, None) } /// Encoded frame from hwcodec (cloned for ownership) @@ -321,7 +313,7 @@ impl H264Encoder { })?; let yuv_length = inner.length; - let encoder_type = codec_name_to_type(codec_name); + let encoder_type = H264EncoderType::from(EncoderBackend::from_codec_name(codec_name)); info!( "H.264 encoder created: {} (type: {}, buffer_length: {}, input_format: {:?})", diff --git a/src/video/encoder/h265.rs b/src/video/encoder/h265.rs index 11eaf537..d6b9404f 100644 --- a/src/video/encoder/h265.rs +++ b/src/video/encoder/h265.rs @@ -15,6 +15,7 @@ use hwcodec::ffmpeg::AVPixelFormat; use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder}; use hwcodec::ffmpeg_ram::CodecInfo; +use super::detect_best_codec_for_format; use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType}; use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig}; use crate::error::{AppError, Result}; @@ -221,43 +222,25 @@ pub fn get_available_h265_encoders(width: u32, height: u32) -> Vec { pub fn detect_best_h265_encoder(width: u32, height: u32) -> (H265EncoderType, Option) { let encoders = get_available_h265_encoders(width, height); - if encoders.is_empty() { - warn!("No H.265 encoders available"); - return (H265EncoderType::None, None); - } - // Prefer hardware encoders over software (libx265) // Hardware priority: NVENC > QSV > AMF > VAAPI > RKMPP > V4L2 M2M > Software - let codec = encoders - .iter() - .find(|e| !e.name.contains("libx265")) - .or_else(|| encoders.first()) - .unwrap(); - - let encoder_type = if codec.name.contains("nvenc") { - H265EncoderType::Nvenc - } else if codec.name.contains("qsv") { - H265EncoderType::Qsv - } else if codec.name.contains("amf") { - H265EncoderType::Amf - } else if codec.name.contains("vaapi") { - H265EncoderType::Vaapi - } else if codec.name.contains("rkmpp") { - H265EncoderType::Rkmpp - } else if codec.name.contains("v4l2m2m") { - H265EncoderType::V4l2M2m + if let Some((encoder_type, codec_name)) = + detect_best_codec_for_format(&encoders, DataFormat::H265, |codec| { + !codec.name.contains("libx265") + }) + { + info!("Selected H.265 encoder: {} ({})", codec_name, encoder_type); + (encoder_type, Some(codec_name)) } else { - H265EncoderType::Software // Default to software for unknown - }; - - info!("Selected H.265 encoder: {} ({})", codec.name, encoder_type); - (encoder_type, Some(codec.name.clone())) + warn!("No H.265 encoders available"); + (H265EncoderType::None, None) + } } /// Check if H265 hardware encoding is available pub fn is_h265_available() -> bool { let registry = EncoderRegistry::global(); - registry.is_format_available(VideoEncoderType::H265, true) + registry.is_codec_available(VideoEncoderType::H265) } /// Encoded frame from hwcodec (cloned for ownership) @@ -268,7 +251,7 @@ pub struct HwEncodeFrame { pub key: i32, } -/// H.265 encoder using hwcodec (hardware only) +/// H.265 encoder using hwcodec pub struct H265Encoder { /// hwcodec encoder instance inner: HwEncoder, diff --git a/src/video/encoder/mod.rs b/src/video/encoder/mod.rs index a0e10fb3..daece2a0 100644 --- a/src/video/encoder/mod.rs +++ b/src/video/encoder/mod.rs @@ -3,12 +3,15 @@ //! This module provides video encoding capabilities including: //! - JPEG encoding for raw frames (YUYV, NV12, etc.) //! - H264 encoding (hardware + software) -//! - H265 encoding (hardware only) -//! - VP8 encoding (hardware only - VAAPI) -//! - VP9 encoding (hardware only - VAAPI) +//! - H265 encoding (hardware + software) +//! - VP8 encoding (hardware + software) +//! - VP9 encoding (hardware + software) //! - WebRTC video codec abstraction //! - Encoder registry for automatic detection +use hwcodec::common::DataFormat; +use hwcodec::ffmpeg_ram::CodecInfo; + pub mod codec; pub mod h264; pub mod h265; @@ -32,14 +35,45 @@ pub use registry::{AvailableEncoder, EncoderBackend, EncoderRegistry, VideoEncod // H264 encoder pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat}; -// H265 encoder (hardware only) +// H265 encoder pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat}; -// VP8 encoder (hardware only) +// VP8 encoder pub use vp8::{VP8Config, VP8Encoder, VP8EncoderType, VP8InputFormat}; -// VP9 encoder (hardware only) +// VP9 encoder pub use vp9::{VP9Config, VP9Encoder, VP9EncoderType, VP9InputFormat}; // JPEG encoder pub use jpeg::JpegEncoder; + +pub(crate) fn select_codec_for_format( + encoders: &[CodecInfo], + format: DataFormat, + preferred: F, +) -> Option<&CodecInfo> +where + F: Fn(&CodecInfo) -> bool, +{ + encoders + .iter() + .find(|codec| codec.format == format && preferred(codec)) + .or_else(|| encoders.iter().find(|codec| codec.format == format)) +} + +pub(crate) fn detect_best_codec_for_format( + encoders: &[CodecInfo], + format: DataFormat, + preferred: F, +) -> Option<(T, String)> +where + T: From, + F: Fn(&CodecInfo) -> bool, +{ + select_codec_for_format(encoders, format, preferred).map(|codec| { + ( + T::from(EncoderBackend::from_codec_name(&codec.name)), + codec.name.clone(), + ) + }) +} diff --git a/src/video/encoder/registry.rs b/src/video/encoder/registry.rs index 5a9658dc..fb3d475f 100644 --- a/src/video/encoder/registry.rs +++ b/src/video/encoder/registry.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::sync::OnceLock; +use std::time::Duration; use tracing::{debug, info, warn}; use hwcodec::common::{DataFormat, Quality, RateControl}; @@ -28,6 +29,10 @@ pub enum VideoEncoderType { } impl VideoEncoderType { + pub const fn ordered() -> [Self; 4] { + [Self::H264, Self::H265, Self::VP8, Self::VP9] + } + /// Convert to hwcodec DataFormat pub fn to_data_format(&self) -> DataFormat { match self { @@ -68,17 +73,6 @@ impl VideoEncoderType { VideoEncoderType::VP9 => "VP9", } } - - /// Check if this format requires hardware-only encoding - /// H264 supports software fallback, others require hardware - pub fn hardware_only(&self) -> bool { - match self { - VideoEncoderType::H264 => false, - VideoEncoderType::H265 => true, - VideoEncoderType::VP8 => true, - VideoEncoderType::VP9 => true, - } - } } impl std::fmt::Display for VideoEncoderType { @@ -210,6 +204,76 @@ pub struct EncoderRegistry { } impl EncoderRegistry { + fn detect_encoders_with_timeout(ctx: EncodeContext, timeout: Duration) -> Vec { + use std::sync::mpsc; + + let (tx, rx) = mpsc::channel(); + let handle = std::thread::Builder::new() + .name("ffmpeg-encoder-detect".to_string()) + .spawn(move || { + let result = HwEncoder::available_encoders(ctx, None); + let _ = tx.send(result); + }); + + let Ok(handle) = handle else { + warn!("Failed to spawn encoder detection thread"); + return Vec::new(); + }; + + match rx.recv_timeout(timeout) { + Ok(encoders) => { + let _ = handle.join(); + encoders + } + Err(mpsc::RecvTimeoutError::Timeout) => { + warn!( + "Encoder detection timed out after {}ms, skipping hardware detection", + timeout.as_millis() + ); + std::thread::spawn(move || { + let _ = handle.join(); + }); + Vec::new() + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + let _ = handle.join(); + warn!("Encoder detection thread exited unexpectedly"); + Vec::new() + } + } + } + + fn register_software_fallbacks(&mut self) { + info!("Registering software encoders..."); + + for format in VideoEncoderType::ordered() { + let encoders = self.encoders.entry(format).or_default(); + if encoders.iter().any(|encoder| !encoder.is_hardware) { + continue; + } + + let codec_name = match format { + VideoEncoderType::H264 => "libx264", + VideoEncoderType::H265 => "libx265", + VideoEncoderType::VP8 => "libvpx", + VideoEncoderType::VP9 => "libvpx-vp9", + }; + + encoders.push(AvailableEncoder { + format, + codec_name: codec_name.to_string(), + backend: EncoderBackend::Software, + priority: 100, + is_hardware: false, + }); + + debug!( + "Registered software encoder: {} for {} (priority: {})", + codec_name, format, 100 + ); + } + } + /// Get the global registry instance /// /// The registry is initialized lazily on first access with 1920x1080 detection. @@ -257,32 +321,11 @@ impl EncoderRegistry { }; const DETECT_TIMEOUT_MS: u64 = 5000; - - // Get all available encoders from hwcodec with a hard timeout - let all_encoders = { - use std::sync::mpsc; - use std::time::Duration; - - info!("Encoder detection timeout: {}ms", DETECT_TIMEOUT_MS); - - let (tx, rx) = mpsc::channel(); - let ctx_clone = ctx.clone(); - std::thread::spawn(move || { - let result = HwEncoder::available_encoders(ctx_clone, None); - let _ = tx.send(result); - }); - - match rx.recv_timeout(Duration::from_millis(DETECT_TIMEOUT_MS)) { - Ok(encoders) => encoders, - Err(_) => { - warn!( - "Encoder detection timed out after {}ms, skipping hardware detection", - DETECT_TIMEOUT_MS - ); - Vec::new() - } - } - }; + info!("Encoder detection timeout: {}ms", DETECT_TIMEOUT_MS); + let all_encoders = Self::detect_encoders_with_timeout( + ctx.clone(), + Duration::from_millis(DETECT_TIMEOUT_MS), + ); info!("Found {} encoders from hwcodec", all_encoders.len()); @@ -305,32 +348,7 @@ impl EncoderRegistry { encoders.sort_by_key(|e| e.priority); } - // Register software encoders as fallback - info!("Registering software encoders..."); - let software_encoders = [ - (VideoEncoderType::H264, "libx264", 100), - (VideoEncoderType::H265, "libx265", 100), - (VideoEncoderType::VP8, "libvpx", 100), - (VideoEncoderType::VP9, "libvpx-vp9", 100), - ]; - - for (format, codec_name, priority) in software_encoders { - self.encoders - .entry(format) - .or_default() - .push(AvailableEncoder { - format, - codec_name: codec_name.to_string(), - backend: EncoderBackend::Software, - priority, - is_hardware: false, - }); - - debug!( - "Registered software encoder: {} for {} (priority: {})", - codec_name, format, priority - ); - } + self.register_software_fallbacks(); // Log summary for (format, encoders) in &self.encoders { @@ -370,6 +388,10 @@ impl EncoderRegistry { ) } + pub fn best_available_encoder(&self, format: VideoEncoderType) -> Option<&AvailableEncoder> { + self.best_encoder(format, false) + } + /// Get all encoders for a format pub fn encoders_for_format(&self, format: VideoEncoderType) -> &[AvailableEncoder] { self.encoders @@ -405,31 +427,17 @@ impl EncoderRegistry { self.best_encoder(format, hardware_only).is_some() } + pub fn is_codec_available(&self, format: VideoEncoderType) -> bool { + self.best_available_encoder(format).is_some() + } + /// Get available formats for user selection /// - /// Returns formats that are actually usable based on their requirements: - /// - H264: Available if any encoder exists (hardware or software) - /// - H265/VP8/VP9: Available only if hardware encoder exists pub fn selectable_formats(&self) -> Vec { - let mut formats = Vec::new(); - - // H264 - supports software fallback - if self.is_format_available(VideoEncoderType::H264, false) { - formats.push(VideoEncoderType::H264); - } - - // H265/VP8/VP9 - hardware only - for format in [ - VideoEncoderType::H265, - VideoEncoderType::VP8, - VideoEncoderType::VP9, - ] { - if self.is_format_available(format, true) { - formats.push(format); - } - } - - formats + VideoEncoderType::ordered() + .into_iter() + .filter(|format| self.is_codec_available(*format)) + .collect() } /// Get detection resolution @@ -534,11 +542,16 @@ mod tests { } #[test] - fn test_hardware_only_requirement() { - assert!(!VideoEncoderType::H264.hardware_only()); - assert!(VideoEncoderType::H265.hardware_only()); - assert!(VideoEncoderType::VP8.hardware_only()); - assert!(VideoEncoderType::VP9.hardware_only()); + fn test_codec_ordering() { + assert_eq!( + VideoEncoderType::ordered(), + [ + VideoEncoderType::H264, + VideoEncoderType::H265, + VideoEncoderType::VP8, + VideoEncoderType::VP9, + ] + ); } #[test] diff --git a/src/video/encoder/vp8.rs b/src/video/encoder/vp8.rs index 453fa133..858489b0 100644 --- a/src/video/encoder/vp8.rs +++ b/src/video/encoder/vp8.rs @@ -15,6 +15,7 @@ use hwcodec::ffmpeg::AVPixelFormat; use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder}; use hwcodec::ffmpeg_ram::CodecInfo; +use super::detect_best_codec_for_format; use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType}; use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig}; use crate::error::{AppError, Result}; @@ -156,32 +157,24 @@ pub fn get_available_vp8_encoders(width: u32, height: u32) -> Vec { pub fn detect_best_vp8_encoder(width: u32, height: u32) -> (VP8EncoderType, Option) { let encoders = get_available_vp8_encoders(width, height); - if encoders.is_empty() { - warn!("No VP8 encoders available"); - return (VP8EncoderType::None, None); - } - // Prefer hardware encoders (VAAPI) over software (libvpx) - let codec = encoders - .iter() - .find(|e| e.name.contains("vaapi")) - .or_else(|| encoders.first()) - .unwrap(); - - let encoder_type = if codec.name.contains("vaapi") { - VP8EncoderType::Vaapi + if let Some((encoder_type, codec_name)) = + detect_best_codec_for_format(&encoders, DataFormat::VP8, |codec| { + codec.name.contains("vaapi") + }) + { + info!("Selected VP8 encoder: {} ({})", codec_name, encoder_type); + (encoder_type, Some(codec_name)) } else { - VP8EncoderType::Software // Default to software for unknown - }; - - info!("Selected VP8 encoder: {} ({})", codec.name, encoder_type); - (encoder_type, Some(codec.name.clone())) + warn!("No VP8 encoders available"); + (VP8EncoderType::None, None) + } } /// Check if VP8 hardware encoding is available pub fn is_vp8_available() -> bool { let registry = EncoderRegistry::global(); - registry.is_format_available(VideoEncoderType::VP8, true) + registry.is_codec_available(VideoEncoderType::VP8) } /// Encoded frame from hwcodec (cloned for ownership) @@ -192,7 +185,7 @@ pub struct HwEncodeFrame { pub key: i32, } -/// VP8 encoder using hwcodec (hardware only - VAAPI) +/// VP8 encoder using hwcodec pub struct VP8Encoder { /// hwcodec encoder instance inner: HwEncoder, diff --git a/src/video/encoder/vp9.rs b/src/video/encoder/vp9.rs index aab23ff3..2f277c08 100644 --- a/src/video/encoder/vp9.rs +++ b/src/video/encoder/vp9.rs @@ -15,6 +15,7 @@ use hwcodec::ffmpeg::AVPixelFormat; use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder}; use hwcodec::ffmpeg_ram::CodecInfo; +use super::detect_best_codec_for_format; use super::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType}; use super::traits::{EncodedFormat, EncodedFrame, Encoder, EncoderConfig}; use crate::error::{AppError, Result}; @@ -156,32 +157,24 @@ pub fn get_available_vp9_encoders(width: u32, height: u32) -> Vec { pub fn detect_best_vp9_encoder(width: u32, height: u32) -> (VP9EncoderType, Option) { let encoders = get_available_vp9_encoders(width, height); - if encoders.is_empty() { - warn!("No VP9 encoders available"); - return (VP9EncoderType::None, None); - } - // Prefer hardware encoders (VAAPI) over software (libvpx-vp9) - let codec = encoders - .iter() - .find(|e| e.name.contains("vaapi")) - .or_else(|| encoders.first()) - .unwrap(); - - let encoder_type = if codec.name.contains("vaapi") { - VP9EncoderType::Vaapi + if let Some((encoder_type, codec_name)) = + detect_best_codec_for_format(&encoders, DataFormat::VP9, |codec| { + codec.name.contains("vaapi") + }) + { + info!("Selected VP9 encoder: {} ({})", codec_name, encoder_type); + (encoder_type, Some(codec_name)) } else { - VP9EncoderType::Software // Default to software for unknown - }; - - info!("Selected VP9 encoder: {} ({})", codec.name, encoder_type); - (encoder_type, Some(codec.name.clone())) + warn!("No VP9 encoders available"); + (VP9EncoderType::None, None) + } } /// Check if VP9 hardware encoding is available pub fn is_vp9_available() -> bool { let registry = EncoderRegistry::global(); - registry.is_format_available(VideoEncoderType::VP9, true) + registry.is_codec_available(VideoEncoderType::VP9) } /// Encoded frame from hwcodec (cloned for ownership) @@ -192,7 +185,7 @@ pub struct HwEncodeFrame { pub key: i32, } -/// VP9 encoder using hwcodec (hardware only - VAAPI) +/// VP9 encoder using hwcodec pub struct VP9Encoder { /// hwcodec encoder instance inner: HwEncoder, diff --git a/src/video/shared_video_pipeline.rs b/src/video/shared_video_pipeline.rs index f34725b9..43a489cb 100644 --- a/src/video/shared_video_pipeline.rs +++ b/src/video/shared_video_pipeline.rs @@ -38,14 +38,12 @@ use crate::error::{AppError, Result}; use crate::utils::LogThrottler; use crate::video::convert::{Nv12Converter, PixelConverter}; use crate::video::decoder::MjpegTurboDecoder; -use crate::video::encoder::h264::{detect_best_encoder, H264Config, H264Encoder, H264InputFormat}; -use crate::video::encoder::h265::{ - detect_best_h265_encoder, H265Config, H265Encoder, H265InputFormat, -}; +use crate::video::encoder::h264::{H264Config, H264Encoder, H264InputFormat}; +use crate::video::encoder::h265::{H265Config, H265Encoder, H265InputFormat}; use crate::video::encoder::registry::{EncoderBackend, EncoderRegistry, VideoEncoderType}; use crate::video::encoder::traits::EncoderConfig; -use crate::video::encoder::vp8::{detect_best_vp8_encoder, VP8Config, VP8Encoder}; -use crate::video::encoder::vp9::{detect_best_vp9_encoder, VP9Config, VP9Encoder}; +use crate::video::encoder::vp8::{VP8Config, VP8Encoder}; +use crate::video::encoder::vp9::{VP9Config, VP9Encoder}; use crate::video::format::{PixelFormat, Resolution}; use crate::video::frame::{FrameBuffer, FrameBufferPool, VideoFrame}; use crate::video::v4l2r_capture::V4l2rCaptureStream; @@ -389,7 +387,7 @@ impl SharedVideoPipeline { .encoder_with_backend(format, b) .map(|e| e.codec_name.clone()), None => registry - .best_encoder(format, false) + .best_available_encoder(format) .map(|e| e.codec_name.clone()), } }; @@ -447,10 +445,7 @@ impl SharedVideoPipeline { )) })? } else { - // Auto select best available encoder - let (_encoder_type, detected) = - detect_best_encoder(config.resolution.width, config.resolution.height); - detected.ok_or_else(|| { + get_codec_name(VideoEncoderType::H264, None).ok_or_else(|| { AppError::VideoError("No H.264 encoder available".to_string()) })? } @@ -472,9 +467,7 @@ impl SharedVideoPipeline { )) })? } else { - let (_encoder_type, detected) = - detect_best_h265_encoder(config.resolution.width, config.resolution.height); - detected.ok_or_else(|| { + get_codec_name(VideoEncoderType::H265, None).ok_or_else(|| { AppError::VideoError("No H.265 encoder available".to_string()) })? } @@ -485,9 +478,7 @@ impl SharedVideoPipeline { AppError::VideoError(format!("Backend {:?} does not support VP8", backend)) })? } else { - let (_encoder_type, detected) = - detect_best_vp8_encoder(config.resolution.width, config.resolution.height); - detected.ok_or_else(|| { + get_codec_name(VideoEncoderType::VP8, None).ok_or_else(|| { AppError::VideoError("No VP8 encoder available".to_string()) })? } @@ -498,9 +489,7 @@ impl SharedVideoPipeline { AppError::VideoError(format!("Backend {:?} does not support VP9", backend)) })? } else { - let (_encoder_type, detected) = - detect_best_vp9_encoder(config.resolution.width, config.resolution.height); - detected.ok_or_else(|| { + get_codec_name(VideoEncoderType::VP9, None).ok_or_else(|| { AppError::VideoError("No VP9 encoder available".to_string()) })? } diff --git a/src/video/video_session.rs b/src/video/video_session.rs index 07af5f01..39512215 100644 --- a/src/video/video_session.rs +++ b/src/video/video_session.rs @@ -191,15 +191,14 @@ impl VideoSessionManager { *self.frame_source.write().await = Some(rx); } - /// Get available codecs based on hardware capabilities + /// Get available codecs based on encoder availability pub fn available_codecs(&self) -> Vec { EncoderRegistry::global().selectable_formats() } /// Check if a codec is available pub fn is_codec_available(&self, codec: VideoEncoderType) -> bool { - let hardware_only = codec.hardware_only(); - EncoderRegistry::global().is_format_available(codec, hardware_only) + EncoderRegistry::global().is_codec_available(codec) } /// Create a new video session @@ -520,7 +519,7 @@ impl VideoSessionManager { /// Get codec info pub fn get_codec_info(&self, codec: VideoEncoderType) -> Option { let registry = EncoderRegistry::global(); - let encoder = registry.best_encoder(codec, codec.hardware_only())?; + let encoder = registry.best_available_encoder(codec)?; Some(CodecInfo { codec_type: codec, diff --git a/src/web/handlers/mod.rs b/src/web/handlers/mod.rs index 90f5fda7..ea3b94f4 100644 --- a/src/web/handlers/mod.rs +++ b/src/web/handlers/mod.rs @@ -1798,7 +1798,7 @@ pub async fn stream_codecs_list() -> Json { }); // Check H264 availability (supports software fallback) - let h264_encoder = registry.best_encoder(VideoEncoderType::H264, false); + let h264_encoder = registry.best_available_encoder(VideoEncoderType::H264); codecs.push(VideoCodecInfo { id: "h264".to_string(), name: "H.264 / WebRTC".to_string(), @@ -1809,7 +1809,7 @@ pub async fn stream_codecs_list() -> Json { }); // Check H265 availability (now supports software too) - let h265_encoder = registry.best_encoder(VideoEncoderType::H265, false); + let h265_encoder = registry.best_available_encoder(VideoEncoderType::H265); codecs.push(VideoCodecInfo { id: "h265".to_string(), name: "H.265 / WebRTC".to_string(), @@ -1820,7 +1820,7 @@ pub async fn stream_codecs_list() -> Json { }); // Check VP8 availability (now supports software too) - let vp8_encoder = registry.best_encoder(VideoEncoderType::VP8, false); + let vp8_encoder = registry.best_available_encoder(VideoEncoderType::VP8); codecs.push(VideoCodecInfo { id: "vp8".to_string(), name: "VP8 / WebRTC".to_string(), @@ -1831,7 +1831,7 @@ pub async fn stream_codecs_list() -> Json { }); // Check VP9 availability (now supports software too) - let vp9_encoder = registry.best_encoder(VideoEncoderType::VP9, false); + let vp9_encoder = registry.best_available_encoder(VideoEncoderType::VP9); codecs.push(VideoCodecInfo { id: "vp9".to_string(), name: "VP9 / WebRTC".to_string(), diff --git a/src/webrtc/webrtc_streamer.rs b/src/webrtc/webrtc_streamer.rs index b3e6e276..2844f990 100644 --- a/src/webrtc/webrtc_streamer.rs +++ b/src/webrtc/webrtc_streamer.rs @@ -221,23 +221,11 @@ impl WebRtcStreamer { use crate::video::encoder::registry::EncoderRegistry; let registry = EncoderRegistry::global(); - let mut codecs = vec![]; - - // H264 always available (has software fallback) - codecs.push(VideoCodecType::H264); - - // Check hardware codecs - if registry.is_format_available(VideoEncoderType::H265, true) { - codecs.push(VideoCodecType::H265); - } - if registry.is_format_available(VideoEncoderType::VP8, true) { - codecs.push(VideoCodecType::VP8); - } - if registry.is_format_available(VideoEncoderType::VP9, true) { - codecs.push(VideoCodecType::VP9); - } - - codecs + VideoEncoderType::ordered() + .into_iter() + .filter(|codec| registry.is_codec_available(*codec)) + .map(Self::encoder_type_to_codec_type) + .collect() } /// Convert VideoCodecType to VideoEncoderType @@ -250,6 +238,15 @@ impl WebRtcStreamer { } } + fn encoder_type_to_codec_type(codec: VideoEncoderType) -> VideoCodecType { + match codec { + VideoEncoderType::H264 => VideoCodecType::H264, + VideoEncoderType::H265 => VideoCodecType::H265, + VideoEncoderType::VP8 => VideoCodecType::VP8, + VideoEncoderType::VP9 => VideoCodecType::VP9, + } + } + fn should_stop_pipeline(session_count: usize, subscriber_count: usize) -> bool { session_count == 0 && subscriber_count == 0 } @@ -577,7 +574,7 @@ impl WebRtcStreamer { VideoCodecType::VP9 => VideoEncoderType::VP9, }; EncoderRegistry::global() - .best_encoder(codec_type, false) + .best_available_encoder(codec_type) .map(|e| e.is_hardware) .unwrap_or(false) }