//! Frame encoding module //! //! Encodes captured frames for transmission. Supports: //! - 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::{video_frame, VideoCodec, VideoFrame}; use anyhow::Result; /// Encoded frame ready for transmission #[derive(Debug)] pub struct EncodedFrame { /// Protobuf video frame message pub frame: VideoFrame, /// Size in bytes after encoding pub size: usize, /// Whether this is a keyframe (full frame) // Set by encoders; not yet read by the transmit path. #[allow(dead_code)] pub is_keyframe: bool, } /// 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; /// Request a keyframe on next encode #[allow(dead_code)] fn request_keyframe(&mut self); /// Get encoder name/type #[allow(dead_code)] fn name(&self) -> &str; } /// 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() { "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> { 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> { 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"); } }