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:
@@ -805,6 +805,7 @@ async fn handle_agent_connection(
|
||||
organization.clone(),
|
||||
site.clone(),
|
||||
status.tags.clone(),
|
||||
status.supports_h264,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -93,6 +93,36 @@ pub struct Session {
|
||||
pub organization: Option<String>, // Company/organization name
|
||||
pub site: Option<String>, // Site/location name
|
||||
pub tags: Vec<String>, // Tags for categorization
|
||||
/// Whether the agent advertised a hardware H.264 encoder (Task 7). Set from
|
||||
/// `AgentStatus.supports_h264`; drives codec negotiation in `select_video_codec`.
|
||||
pub supports_h264: bool,
|
||||
}
|
||||
|
||||
/// Default codec-negotiation policy (Task 7).
|
||||
///
|
||||
/// `false` means: even when an agent advertises hardware H.264 support, the
|
||||
/// server still negotiates RAW. H.264 is compile-verified only and not yet
|
||||
/// validated on real hardware (plan Task 8), so we deliberately do NOT ship it
|
||||
/// as the default — raw+Zstd stays the guaranteed working path. Flip this to
|
||||
/// `true` once H.264 is live-validated, or make it per-tenant policy later.
|
||||
pub const DEFAULT_PREFER_H264: bool = false;
|
||||
|
||||
/// Negotiate the video codec for a stream (Task 7).
|
||||
///
|
||||
/// Pure decision function (unit-tested): given whether the agent advertised
|
||||
/// hardware H.264 and whether policy prefers H.264, pick the codec. H.264 is
|
||||
/// chosen ONLY when both the agent supports it AND policy allows it; otherwise
|
||||
/// raw — the safe default/fallback. HEVC is intentionally never selected here
|
||||
/// (future opt-in; TODO).
|
||||
pub fn select_video_codec(
|
||||
agent_supports_h264: bool,
|
||||
prefer_h264: bool,
|
||||
) -> crate::proto::VideoCodec {
|
||||
if agent_supports_h264 && prefer_h264 {
|
||||
crate::proto::VideoCodec::H264
|
||||
} else {
|
||||
crate::proto::VideoCodec::Raw
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel for sending frames from agent to viewers
|
||||
@@ -206,6 +236,7 @@ impl SessionManager {
|
||||
organization: None,
|
||||
site: None,
|
||||
tags: Vec::new(),
|
||||
supports_h264: false,
|
||||
};
|
||||
|
||||
let session_data = SessionData {
|
||||
@@ -240,12 +271,14 @@ impl SessionManager {
|
||||
organization: Option<String>,
|
||||
site: Option<String>,
|
||||
tags: Vec<String>,
|
||||
supports_h264: bool,
|
||||
) {
|
||||
let mut sessions = self.sessions.write().await;
|
||||
if let Some(session_data) = sessions.get_mut(&session_id) {
|
||||
session_data.info.last_heartbeat = chrono::Utc::now();
|
||||
session_data.last_heartbeat_instant = Instant::now();
|
||||
session_data.info.is_streaming = is_streaming;
|
||||
session_data.info.supports_h264 = supports_h264;
|
||||
if let Some(os) = os_version {
|
||||
session_data.info.os_version = Some(os);
|
||||
}
|
||||
@@ -409,10 +442,23 @@ impl SessionManager {
|
||||
use crate::proto;
|
||||
use prost::Message;
|
||||
|
||||
// Negotiate the video codec for this stream (Task 7): H.264 only when the
|
||||
// agent advertised hardware support AND policy prefers it. With
|
||||
// DEFAULT_PREFER_H264 = false this always resolves to RAW today (H.264 is
|
||||
// compile-verified only, validated on hardware in Task 8).
|
||||
let codec = select_video_codec(session_data.info.supports_h264, DEFAULT_PREFER_H264);
|
||||
tracing::info!(
|
||||
"StartStream codec negotiation: agent_supports_h264={}, prefer_h264={} -> {:?}",
|
||||
session_data.info.supports_h264,
|
||||
DEFAULT_PREFER_H264,
|
||||
codec
|
||||
);
|
||||
|
||||
let start_stream = proto::Message {
|
||||
payload: Some(proto::message::Payload::StartStream(proto::StartStream {
|
||||
viewer_id: viewer_id.to_string(),
|
||||
display_id: 0, // Primary display
|
||||
video_codec: codec as i32,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -618,6 +664,7 @@ impl SessionManager {
|
||||
organization: None,
|
||||
site: None,
|
||||
tags: Vec::new(),
|
||||
supports_h264: false,
|
||||
};
|
||||
|
||||
// Create placeholder channels (will be replaced on reconnect)
|
||||
@@ -717,6 +764,69 @@ mod tests {
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_negotiation_picks_h264_only_when_supported_and_preferred() {
|
||||
use crate::proto::VideoCodec;
|
||||
|
||||
// Agent supports H.264 AND policy prefers it -> H.264.
|
||||
assert_eq!(select_video_codec(true, true), VideoCodec::H264);
|
||||
// Agent supports it but policy does not prefer it -> raw (the safe default).
|
||||
assert_eq!(select_video_codec(true, false), VideoCodec::Raw);
|
||||
// Policy prefers H.264 but the agent has no HW encoder -> raw.
|
||||
assert_eq!(select_video_codec(false, true), VideoCodec::Raw);
|
||||
// Neither -> raw.
|
||||
assert_eq!(select_video_codec(false, false), VideoCodec::Raw);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_policy_does_not_prefer_h264() {
|
||||
// Guardrail: until H.264 is hardware-validated (Task 8) the default policy
|
||||
// MUST keep raw as the negotiated codec even for capable agents. We assert
|
||||
// the OBSERVABLE behavior (codec selection under the default policy) rather
|
||||
// than the constant directly, which keeps the test meaningful if the policy
|
||||
// later becomes dynamic.
|
||||
let chosen = select_video_codec(true, DEFAULT_PREFER_H264);
|
||||
assert_eq!(
|
||||
chosen,
|
||||
crate::proto::VideoCodec::Raw,
|
||||
"default policy must negotiate raw until H.264 is hardware-validated"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_status_updates_h264_capability() {
|
||||
let mgr = SessionManager::new();
|
||||
let (session_id, _frame_tx, _input_rx) = mgr
|
||||
.register_agent("agent-cap".to_string(), "Cap PC".to_string(), true)
|
||||
.await;
|
||||
|
||||
// Default is false until a status reports capability.
|
||||
assert_eq!(
|
||||
mgr.get_session(session_id).await.map(|s| s.supports_h264),
|
||||
Some(false)
|
||||
);
|
||||
|
||||
mgr.update_agent_status(
|
||||
session_id,
|
||||
Some("Windows".to_string()),
|
||||
true,
|
||||
10,
|
||||
1,
|
||||
false,
|
||||
Some("0.2.0".to_string()),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
true, // supports_h264
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
mgr.get_session(session_id).await.map(|s| s.supports_h264),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn denied_attended_session_keeps_viewer_blocked() {
|
||||
let mgr = SessionManager::new();
|
||||
|
||||
Reference in New Issue
Block a user