feat(agent,server): v2 secure-session-core Task 7 - HW H.264 + negotiated raw fallback
All checks were successful
All checks were successful
SPEC-002 Phase 1 Task 7 (the last), code-reviewed APPROVED, locally verified (cargo fmt + clippy -D warnings exit 0 + cargo test --workspace 89 pass + build). - Encoder trait + factory: RawEncoder (salvaged, UNCHANGED) and H264Encoder, selected by negotiation; factory falls back to raw on H.264 init failure. - Negotiation: agent advertises supports_h264 (MFTEnumEx HW probe, cached) in AgentStatus; server picks the codec via select_video_codec(supports, prefer) and stamps StartStream.video_codec; agent re-guards on local HW. Policy constant DEFAULT_PREFER_H264 = false, so RAW is negotiated for every session today - H.264 stays dormant until live hardware validation (Task 8). - MF H.264 encoder (h264.rs, FIRST-CUT / compile-verified-only): HW encoder MFT, BGRA->NV12 (color.rs, unit-tested), sync drain, fall-back-to-raw on any failure. - Viewer H.264 decoder (decoder.rs, FIRST-CUT): MF decoder on a dedicated COM thread; drops+logs on failure, raw render path untouched. - proto additive: VideoCodec enum, StartStream.video_codec=3, SessionResponse.video_codec=5, AgentStatus.supports_h264=11. - Raw+Zstd path byte-for-byte unchanged; remains the guaranteed default/fallback. Review confirmed unsafe impl Send for H264Encoder is sound (single-owned &mut on the block_on thread; session future never spawned) and every MF failure degrades to raw. H.264 is NOT claimed functional - compile/clippy/build-verified only; live validation + force-IDR + the no-spawn-invariant doc are Task 8 go-live gates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
agent/src/encoder/capability.rs
Normal file
97
agent/src/encoder/capability.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Hardware video-encode capability detection (Task 7).
|
||||
//!
|
||||
//! Probes Windows Media Foundation for a HARDWARE H.264 encoder MFT at startup.
|
||||
//! The result is cached and advertised to the server in `AgentStatus.supports_h264`
|
||||
//! so the server can negotiate the codec (see `StartStream.video_codec`).
|
||||
//!
|
||||
//! Detection is intentionally cheap and side-effect-free: it only ENUMERATES the
|
||||
//! available encoder MFTs (it does not create or initialize one). A `true` result
|
||||
//! means a hardware H.264 encoder was advertised by the OS; it does NOT guarantee
|
||||
//! the encoder will successfully initialize at stream time — the H.264 encoder
|
||||
//! still falls back to raw on any init/feed failure.
|
||||
//!
|
||||
//! On non-Windows targets, or if MF is unavailable, this reports `false`.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Cached capability result. Detection runs at most once per process.
|
||||
static SUPPORTS_H264: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
/// Return whether this machine has a hardware H.264 encoder, detecting once and
|
||||
/// caching the result. Safe to call repeatedly and from any thread.
|
||||
pub fn supports_hardware_h264() -> bool {
|
||||
*SUPPORTS_H264.get_or_init(detect_hardware_h264)
|
||||
}
|
||||
|
||||
/// Run the actual detection. Separated so the cached accessor stays trivial.
|
||||
fn detect_hardware_h264() -> bool {
|
||||
let supported = detect_inner();
|
||||
if supported {
|
||||
tracing::info!("Hardware H.264 encoder detected (Media Foundation)");
|
||||
} else {
|
||||
tracing::info!("No hardware H.264 encoder detected; raw+Zstd only");
|
||||
}
|
||||
supported
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn detect_inner() -> bool {
|
||||
// Enumerate hardware H.264 encoder MFTs. This is a read-only probe; it does
|
||||
// not init D3D, COM apartments persistently, or create the encoder.
|
||||
match unsafe { enumerate_hardware_h264() } {
|
||||
Ok(found) => found,
|
||||
Err(e) => {
|
||||
tracing::warn!("H.264 capability probe failed: {e:#}; assuming no HW encoder");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn detect_inner() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
unsafe fn enumerate_hardware_h264() -> anyhow::Result<bool> {
|
||||
use windows::Win32::Media::MediaFoundation::{
|
||||
MFMediaType_Video, MFTEnumEx, MFVideoFormat_H264, MFT_CATEGORY_VIDEO_ENCODER,
|
||||
MFT_ENUM_FLAG_HARDWARE, MFT_ENUM_FLAG_SORTANDFILTER, MFT_ENUM_FLAG_TRANSCODE_ONLY,
|
||||
MFT_REGISTER_TYPE_INFO,
|
||||
};
|
||||
|
||||
// We only specify the OUTPUT type (H.264); input is left unconstrained so the
|
||||
// probe matches encoders regardless of their preferred input subtype.
|
||||
let output_type = MFT_REGISTER_TYPE_INFO {
|
||||
guidMajorType: MFMediaType_Video,
|
||||
guidSubtype: MFVideoFormat_H264,
|
||||
};
|
||||
|
||||
let mut activate_ptr: *mut Option<windows::Win32::Media::MediaFoundation::IMFActivate> =
|
||||
std::ptr::null_mut();
|
||||
let mut count: u32 = 0;
|
||||
|
||||
// MFTEnumEx does not itself require MFStartup for a pure enumeration, but we
|
||||
// guard with a Result so any HRESULT failure degrades to "no HW encoder".
|
||||
MFTEnumEx(
|
||||
MFT_CATEGORY_VIDEO_ENCODER,
|
||||
MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_SORTANDFILTER | MFT_ENUM_FLAG_TRANSCODE_ONLY,
|
||||
None, // input type: any
|
||||
Some(&output_type as *const _),
|
||||
&mut activate_ptr,
|
||||
&mut count,
|
||||
)?;
|
||||
|
||||
// Release every returned IMFActivate, then free the array CoTaskMemAlloc'd by MF.
|
||||
let found = count > 0;
|
||||
if !activate_ptr.is_null() {
|
||||
let slice = std::slice::from_raw_parts_mut(activate_ptr, count as usize);
|
||||
for entry in slice.iter_mut() {
|
||||
// Dropping the Option<IMFActivate> releases the COM reference.
|
||||
entry.take();
|
||||
}
|
||||
windows::Win32::System::Com::CoTaskMemFree(Some(activate_ptr as *const _));
|
||||
}
|
||||
|
||||
Ok(found)
|
||||
}
|
||||
Reference in New Issue
Block a user