NetDesk 1.5.0 — source release
This commit is contained in:
commit
21dea33089
227 files changed
+80714
No files matched your search
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user