Files
netdesk/native/host/src/encode.rs
T

75 lines
2.9 KiB
Rust

//! H.264 encode via openh264 (pure-Rust-friendly, statically linked C core) —
//! the portable software path that works on every Windows PC with no driver or
//! GPU dependency. BGRA (from DXGI capture) is converted to I420 and encoded as
//! Annex-B access units: keyframes carry SPS+PPS+IDR, deltas carry a P slice,
//! exactly what the viewer's openh264/Media Foundation decoder expects.
//!
//! Hardware encoders (NVENC / QSV / AMF) layer in later behind this same
//! `EncodedFrame` output; software-first guarantees the host always works.
use anyhow::{anyhow, Result};
use openh264::encoder::{Encoder, EncoderConfig, FrameType, RateControlMode, UsageType};
use openh264::formats::{BgraSliceU8, YUVBuffer};
use openh264::OpenH264API;
/// One encoded access unit ready to hand to `core::host`, which prefixes the
/// video channel + keyframe byte and encrypts it.
pub struct EncodedFrame {
pub data: Vec<u8>,
pub keyframe: bool,
}
/// A live H.264 encoder for one resolution. Resolution changes re-init the
/// encoder automatically (openh264 does this on dimension change).
pub struct H264Encoder {
enc: Encoder,
width: usize,
height: usize,
}
impl H264Encoder {
pub fn new(width: usize, height: usize, fps: u32, bitrate_bps: u32) -> Result<Self> {
let config = EncoderConfig::new()
.set_bitrate_bps(bitrate_bps)
.max_frame_rate(fps as f32)
.rate_control_mode(RateControlMode::Bitrate)
.usage_type(UsageType::ScreenContentRealTime)
.enable_skip_frame(true)
.set_multiple_thread_idc(0); // auto: use all cores
let enc = Encoder::with_api_config(OpenH264API::from_source(), config)
.map_err(|e| anyhow!("openh264 encoder init: {e}"))?;
Ok(Self { enc, width, height })
}
/// Encode one tightly-packed BGRA frame (`width*height*4`). Returns `None`
/// when rate control skipped this frame (nothing to send this tick).
pub fn encode(&mut self, bgra: &[u8]) -> Result<Option<EncodedFrame>> {
let src = BgraSliceU8::new(bgra, (self.width, self.height));
let yuv = YUVBuffer::from_rgb_source(src);
let bs = self.enc.encode(&yuv).map_err(|e| anyhow!("encode: {e}"))?;
let ft = bs.frame_type();
if ft == FrameType::Skip || ft == FrameType::Invalid {
return Ok(None);
}
let data = bs.to_vec();
if data.is_empty() {
return Ok(None);
}
let keyframe = matches!(ft, FrameType::IDR | FrameType::I);
Ok(Some(EncodedFrame { data, keyframe }))
}
/// Force the next encoded frame to be a keyframe (viewer asked for a refresh
/// after a drop, or a new viewer joined).
pub fn request_keyframe(&mut self) {
self.enc.force_intra_frame();
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
}