//! Unified video encoder: prefer the platform hardware encoder (Media //! Foundation NVENC/QSV/AMF on Windows), fall back to the portable openh264 //! software encoder, or — on constrained links — VP9 software (libvpx), which //! is far better per-bitrate than H.264. One `encode` call returns 0+ coded //! frames (the async hardware path can run a frame of latency; software is 1:1). use crate::encode::{EncodedFrame, H264Encoder}; use anyhow::Result; pub enum VideoEncoder { #[cfg(windows)] Hardware(crate::encode_mf::MfEncoder), Software(H264Encoder), #[cfg(windows)] Vp9(Vp9State), } /// VP9 encoder state. libvpx is stateless about wall-clock, so we track a start /// instant (ms PTS) and a pending-keyframe flag (the encode loop asks for a /// keyframe on viewer join / refresh / re-init). #[cfg(windows)] pub struct Vp9State { enc: netdesk_vpx::Encoder, want_key: bool, start: std::time::Instant, } impl VideoEncoder { /// Build the best available H.264 encoder for this machine (hardware MF, /// else openh264 software). This is the default fast-link path. pub fn best(width: usize, height: usize, fps: u32, bitrate_bps: u32) -> Result { #[cfg(windows)] { match crate::encode_mf::MfEncoder::new(width, height, fps, bitrate_bps) { Ok(e) => { log::info!("encode: hardware \"{}\"", e.name()); return Ok(VideoEncoder::Hardware(e)); } Err(e) => { log::warn!("encode: no hardware encoder ({e}); using openh264 software"); } } } let e = H264Encoder::new(width, height, fps, bitrate_bps)?; log::info!("encode: openh264 software"); Ok(VideoEncoder::Software(e)) } /// Build an encoder for a specific codec (`proto::VideoCodec`): VP9 (1) uses /// libvpx software; anything else falls back to [`best`] (H.264). VP9 needs /// even dimensions, so callers must pass even `width`/`height` (the capture /// loop uses `color::scaled_even`). pub fn with_codec( codec_id: u8, i444: bool, width: usize, height: usize, fps: u32, bitrate_bps: u32, ) -> Result { #[cfg(windows)] { if codec_id == 1 { let kbps = (bitrate_bps / 1000).max(50); match netdesk_vpx::Encoder::new_i444( netdesk_vpx::VpxCodec::Vp9, width as u32, height as u32, fps, kbps, i444, ) { Ok(enc) => { log::info!( "encode: VP9 software (libvpx) {width}x{height} @ {kbps} kbps{}", if i444 { " 4:4:4" } else { "" } ); return Ok(VideoEncoder::Vp9(Vp9State { enc, want_key: true, // first frame is a keyframe anyway start: std::time::Instant::now(), })); } Err(e) => { log::warn!("encode: VP9 init failed ({e}); using H.264"); } } } } let _ = codec_id; Self::best(width, height, fps, bitrate_bps) } pub fn encode(&mut self, bgra: &[u8]) -> Result> { match self { #[cfg(windows)] VideoEncoder::Hardware(e) => e.encode(bgra), VideoEncoder::Software(e) => Ok(e.encode(bgra)?.into_iter().collect()), #[cfg(windows)] VideoEncoder::Vp9(s) => { let pts = s.start.elapsed().as_millis() as i64; let force_key = std::mem::take(&mut s.want_key); let frames = s.enc.encode_bgra(bgra, pts, force_key)?; Ok(frames .into_iter() .map(|f| EncodedFrame { data: f.data, keyframe: f.key, }) .collect()) } } } pub fn request_keyframe(&mut self) { match self { #[cfg(windows)] VideoEncoder::Hardware(e) => e.request_keyframe(), VideoEncoder::Software(e) => e.request_keyframe(), #[cfg(windows)] VideoEncoder::Vp9(s) => s.want_key = true, } } /// Codec id for the frames this encoder emits (`proto::VideoCodec`): /// 0 = H.264 (hardware MF / openh264 software), 1 = VP9. pub fn codec_id(&self) -> u8 { match self { #[cfg(windows)] VideoEncoder::Hardware(_) => 0, VideoEncoder::Software(_) => 0, #[cfg(windows)] VideoEncoder::Vp9(_) => 1, } } pub fn is_hardware(&self) -> bool { match self { #[cfg(windows)] VideoEncoder::Hardware(_) => true, VideoEncoder::Software(_) => false, #[cfg(windows)] VideoEncoder::Vp9(_) => false, } } pub fn label(&self) -> String { match self { #[cfg(windows)] VideoEncoder::Hardware(e) => format!("hardware: {}", e.name()), VideoEncoder::Software(_) => "software: openh264".to_string(), #[cfg(windows)] VideoEncoder::Vp9(_) => "software: VP9 (libvpx)".to_string(), } } } #[cfg(all(test, windows))] mod tests { use super::*; use netdesk_core::decode::best_decoder_for; // End-to-end through the real host encoder wrapper + the core decoder // selector: BGRA -> VideoEncoder(VP9) -> best_decoder_for(1) -> RGB, proving // the codec_id tag, pts/keyframe handling, and the BGRA<->RGB conversions // all line up (not just the raw netdesk-vpx roundtrip). #[test] fn vp9_encode_decode_via_video_encoder() { let (w, h) = (128usize, 96usize); let mut bgra = vec![0u8; w * h * 4]; for j in 0..h { for i in 0..w { let p = (j * w + i) * 4; let (b, g, r) = if j < h / 2 && i < w / 2 { (30u8, 30, 200) // red } else if j < h / 2 { (30, 200, 30) // green } else if i < w / 2 { (200, 30, 30) // blue } else { (200, 200, 200) // gray }; bgra[p] = b; bgra[p + 1] = g; bgra[p + 2] = r; bgra[p + 3] = 255; } } let mut enc = VideoEncoder::with_codec(1, false, w, h, 30, 2_000_000).expect("vp9 encoder"); assert_eq!(enc.codec_id(), 1, "must be VP9"); let frames = enc.encode(&bgra).expect("encode"); assert!(!frames.is_empty(), "no VP9 packets"); assert!(frames[0].keyframe, "first VP9 frame must be a keyframe"); let mut dec = best_decoder_for(1); let mut got = None; for f in &frames { if let Ok(Some(fr)) = dec.decode(&f.data) { got = Some(fr); } } let fr = got.expect("decoded a frame"); assert_eq!((fr.w, fr.h), (w, h), "decoded dims"); let sample = |x: usize, y: usize| { let p = (y * w + x) * 3; (fr.rgb[p], fr.rgb[p + 1], fr.rgb[p + 2]) // (R, G, B) }; let approx = |a: u8, b: u8| (a as i32 - b as i32).abs() <= 48; let (r, g, b) = sample(w / 4, h / 4); // red quadrant assert!( approx(r, 200) && approx(g, 30) && approx(b, 30), "red quad decoded as r{r} g{g} b{b}" ); println!("VP9_E2E_OK {w}x{h} key_bytes={}", frames[0].data.len()); } }