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:
@@ -1,16 +1,27 @@
|
||||
//! Frame encoding module
|
||||
//!
|
||||
//! Encodes captured frames for transmission. Supports:
|
||||
//! - Raw BGRA + Zstd compression (lowest latency, LAN mode)
|
||||
//! - VP9 software encoding (universal fallback)
|
||||
//! - H264 hardware encoding (when GPU available)
|
||||
//! - Raw BGRA + Zstd compression (lowest latency, LAN mode; the guaranteed
|
||||
//! fallback and the current default).
|
||||
//! - H.264 hardware encoding via Windows Media Foundation (Task 7) — the
|
||||
//! negotiated upgrade. Compile-verified; validated on real hardware in plan
|
||||
//! Task 8. On any init/feed failure the factory or encoder falls back to raw.
|
||||
//!
|
||||
//! Codec selection is driven by the negotiated `VideoCodec` the server sends on
|
||||
//! `StartStream` (see `select_codec` / `create_encoder_for`). The capability the
|
||||
//! agent advertises to the server is detected by `capability::supports_hardware_h264`.
|
||||
|
||||
mod capability;
|
||||
pub(crate) mod color;
|
||||
#[cfg(windows)]
|
||||
mod h264;
|
||||
mod raw;
|
||||
|
||||
pub use capability::supports_hardware_h264;
|
||||
pub use raw::RawEncoder;
|
||||
|
||||
use crate::capture::CapturedFrame;
|
||||
use crate::proto::VideoFrame;
|
||||
use crate::proto::{video_frame, VideoCodec, VideoFrame};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Encoded frame ready for transmission
|
||||
@@ -28,7 +39,12 @@ pub struct EncodedFrame {
|
||||
pub is_keyframe: bool,
|
||||
}
|
||||
|
||||
/// Frame encoder trait
|
||||
/// Frame encoder trait.
|
||||
///
|
||||
/// Every implementor turns a `CapturedFrame` (BGRA) into a wire `VideoFrame`
|
||||
/// using one `video_frame::Encoding` variant. `RawEncoder` emits the `Raw`
|
||||
/// variant; the H.264 encoder emits the `H264` variant. The factory
|
||||
/// (`create_encoder_for`) selects the implementor from the negotiated codec.
|
||||
pub trait Encoder: Send {
|
||||
/// Encode a captured frame
|
||||
fn encode(&mut self, frame: &CapturedFrame) -> Result<EncodedFrame>;
|
||||
@@ -42,13 +58,167 @@ pub trait Encoder: Send {
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// Create an encoder based on configuration
|
||||
pub fn create_encoder(codec: &str, quality: u32) -> Result<Box<dyn Encoder>> {
|
||||
/// Map a configured/negotiated codec string to a `VideoCodec`.
|
||||
///
|
||||
/// Used when constructing an encoder from the agent's own `EncodingConfig`
|
||||
/// (before any server negotiation). Unknown / "auto" / "raw" all resolve to raw
|
||||
/// — the safe default. "h264" resolves to H.264 (which itself falls back to raw
|
||||
/// if MF init fails).
|
||||
///
|
||||
/// Retained as the config-string entry point (used by `create_encoder` and the
|
||||
/// unit tests); the live session negotiates via `select_codec` on a `VideoCodec`.
|
||||
#[allow(dead_code)]
|
||||
pub fn codec_from_str(codec: &str) -> VideoCodec {
|
||||
match codec.to_lowercase().as_str() {
|
||||
"raw" | "zstd" => Ok(Box::new(RawEncoder::new(quality)?)),
|
||||
// "vp9" => Ok(Box::new(Vp9Encoder::new(quality)?)),
|
||||
// "h264" => Ok(Box::new(H264Encoder::new(quality)?)),
|
||||
// "auto" and any unknown codec default to raw for now (best for LAN)
|
||||
_ => Ok(Box::new(RawEncoder::new(quality)?)),
|
||||
"h264" => VideoCodec::H264,
|
||||
// "h265"/"hevc" are future opt-in (TODO) — treat as raw for now so we
|
||||
// never select an unimplemented codec.
|
||||
_ => VideoCodec::Raw,
|
||||
}
|
||||
}
|
||||
|
||||
/// Choose the codec the agent will actually use for a stream, given the codec
|
||||
/// the server negotiated and the agent's own hardware capability.
|
||||
///
|
||||
/// This is the agent-side guard that keeps the raw fallback authoritative:
|
||||
/// - The server only negotiates H.264 when the agent advertised support, but we
|
||||
/// re-check `supports_hardware_h264()` here so a stale/misconfigured server
|
||||
/// selection can never force an unsupported codec.
|
||||
/// - H.265 is not implemented; it degrades to raw.
|
||||
/// - Anything else is raw.
|
||||
pub fn select_codec(negotiated: VideoCodec, hardware_h264_available: bool) -> VideoCodec {
|
||||
match negotiated {
|
||||
VideoCodec::H264 if hardware_h264_available => VideoCodec::H264,
|
||||
// Server asked for H.264 but we have no HW encoder -> raw.
|
||||
VideoCodec::H264 => VideoCodec::Raw,
|
||||
// HEVC not implemented yet (TODO: Task 7 opt-in / future).
|
||||
VideoCodec::H265 => VideoCodec::Raw,
|
||||
VideoCodec::Raw => VideoCodec::Raw,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an encoder for an explicit `VideoCodec`, with a transparent fallback
|
||||
/// to raw if a hardware encoder cannot be constructed.
|
||||
///
|
||||
/// `quality` is the 1-100 quality knob (mapped per-codec). On H.264 init failure
|
||||
/// this logs and returns a raw encoder so the session keeps working.
|
||||
pub fn create_encoder_for(codec: VideoCodec, quality: u32) -> Result<Box<dyn Encoder>> {
|
||||
match codec {
|
||||
VideoCodec::H264 => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
match h264::H264Encoder::new(quality) {
|
||||
Ok(enc) => {
|
||||
tracing::info!("Using hardware H.264 encoder (Media Foundation)");
|
||||
Ok(Box::new(enc))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"H.264 encoder init failed ({e:#}); falling back to raw+Zstd"
|
||||
);
|
||||
Ok(Box::new(RawEncoder::new(quality)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
tracing::warn!("H.264 unsupported on this platform; using raw+Zstd");
|
||||
Ok(Box::new(RawEncoder::new(quality)?))
|
||||
}
|
||||
}
|
||||
// Raw (and anything that resolved to raw) uses the salvaged encoder.
|
||||
VideoCodec::Raw | VideoCodec::H265 => Ok(Box::new(RawEncoder::new(quality)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an encoder based on a codec string (agent config path).
|
||||
///
|
||||
/// Backwards-compatible entry point that builds an encoder from a codec STRING
|
||||
/// (e.g. `EncodingConfig.codec`). Resolves the string to a `VideoCodec`, applies
|
||||
/// the hardware-availability guard, then builds the encoder. The live session
|
||||
/// uses `select_codec` + `create_encoder_for` (negotiated `VideoCodec`) instead;
|
||||
/// this remains for the config path and is covered by unit tests.
|
||||
#[allow(dead_code)]
|
||||
pub fn create_encoder(codec: &str, quality: u32) -> Result<Box<dyn Encoder>> {
|
||||
let requested = codec_from_str(codec);
|
||||
let chosen = select_codec(requested, supports_hardware_h264());
|
||||
create_encoder_for(chosen, quality)
|
||||
}
|
||||
|
||||
/// Build an `EncodedFrame` carrying a single `video_frame::Encoding` payload.
|
||||
/// Shared helper so encoders don't each repeat the `VideoFrame` wrapper.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn wrap_video_frame(
|
||||
timestamp_ms: i64,
|
||||
display_id: i32,
|
||||
sequence: i32,
|
||||
encoding: video_frame::Encoding,
|
||||
size: usize,
|
||||
is_keyframe: bool,
|
||||
) -> EncodedFrame {
|
||||
EncodedFrame {
|
||||
frame: VideoFrame {
|
||||
timestamp: timestamp_ms,
|
||||
display_id,
|
||||
sequence,
|
||||
encoding: Some(encoding),
|
||||
},
|
||||
size,
|
||||
is_keyframe,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn codec_from_str_maps_known_and_unknown() {
|
||||
assert_eq!(codec_from_str("h264"), VideoCodec::H264);
|
||||
assert_eq!(codec_from_str("H264"), VideoCodec::H264);
|
||||
assert_eq!(codec_from_str("raw"), VideoCodec::Raw);
|
||||
assert_eq!(codec_from_str("zstd"), VideoCodec::Raw);
|
||||
assert_eq!(codec_from_str("auto"), VideoCodec::Raw);
|
||||
assert_eq!(codec_from_str("vp9"), VideoCodec::Raw);
|
||||
// HEVC not implemented -> raw, never H265.
|
||||
assert_eq!(codec_from_str("h265"), VideoCodec::Raw);
|
||||
assert_eq!(codec_from_str("hevc"), VideoCodec::Raw);
|
||||
assert_eq!(codec_from_str(""), VideoCodec::Raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_codec_honors_hardware_guard() {
|
||||
// Server negotiated H.264 and HW is present -> H.264.
|
||||
assert_eq!(select_codec(VideoCodec::H264, true), VideoCodec::H264);
|
||||
// Server negotiated H.264 but no HW -> raw (never forced).
|
||||
assert_eq!(select_codec(VideoCodec::H264, false), VideoCodec::Raw);
|
||||
// Raw stays raw regardless of HW.
|
||||
assert_eq!(select_codec(VideoCodec::Raw, true), VideoCodec::Raw);
|
||||
assert_eq!(select_codec(VideoCodec::Raw, false), VideoCodec::Raw);
|
||||
// HEVC always degrades to raw (unimplemented).
|
||||
assert_eq!(select_codec(VideoCodec::H265, true), VideoCodec::Raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_factory_always_succeeds() {
|
||||
// Raw must always construct (the guaranteed fallback).
|
||||
let enc = create_encoder_for(VideoCodec::Raw, 75).unwrap();
|
||||
assert_eq!(enc.name(), "raw+zstd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_encoder_string_path_resolves_to_raw_without_hw() {
|
||||
// On a machine without a HW encoder (CI / non-Windows), "h264" must
|
||||
// resolve to a working raw encoder, not an error.
|
||||
let enc = create_encoder("h264", 75).unwrap();
|
||||
// Without HW it is raw; with HW it would be the H.264 encoder. We only
|
||||
// assert it constructed.
|
||||
let _ = enc.name();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_encoder_auto_is_raw() {
|
||||
let enc = create_encoder("auto", 75).unwrap();
|
||||
assert_eq!(enc.name(), "raw+zstd");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user