Files
guru-connect/agent/src/encoder/capability.rs
Mike Swanson f9bdecbfdb
All checks were successful
Build and Test / Build Agent (Windows) (push) Successful in 6m57s
Build and Test / Build Server (Linux) (push) Successful in 10m23s
Build and Test / Security Audit (push) Successful in 4m15s
Build and Test / Build Summary (push) Successful in 9s
feat(agent,server): v2 secure-session-core Task 7 - HW H.264 + negotiated raw fallback
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>
2026-05-30 10:35:04 -07:00

98 lines
3.6 KiB
Rust

//! 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)
}