75 lines
2.9 KiB
Rust
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
|
|
}
|
|
}
|