NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 00:47:49 +00:00
commit 21dea33089
227 files changed
+80714

No files matched your search

+218
View File
@@ -0,0 +1,218 @@
//! 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<Self> {
#[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<Self> {
#[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<Vec<EncodedFrame>> {
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());
}
}