use std::sync::Arc;
use bytes::Bytes;
use cudarc::driver::CudaContext;
#[cfg(feature = "nvdec")]
use moq_nvenc::sys::nvEncodeAPI::NV_ENC_INPUT_RESOURCE_TYPE;
use moq_nvenc::sys::nvEncodeAPI::{
GUID, NV_ENC_BUFFER_FORMAT, NV_ENC_CODEC_H264_GUID, NV_ENC_CODEC_HEVC_GUID, NV_ENC_PARAMS_RC_MODE,
NV_ENC_PRESET_P4_GUID, NV_ENC_TUNING_INFO,
};
use moq_nvenc::{Encoder, EncoderInitParams, Session};
use super::super::encoder::{Codec, Config};
use super::Backend;
use crate::Error;
use crate::frame::{Frame, interleave_uv};
pub(crate) const NAME: &str = "nvenc";
fn codec_guid(codec: Codec) -> GUID {
match codec {
Codec::H264 => NV_ENC_CODEC_H264_GUID,
Codec::H265 => NV_ENC_CODEC_HEVC_GUID,
}
}
pub(crate) struct Nvenc {
session: Session,
_cuda: Arc<CudaContext>,
timestamp: u64,
}
unsafe impl Send for Nvenc {}
impl Nvenc {
pub(crate) fn open(config: &Config) -> Result<Box<dyn Backend>, Error> {
if !driver_libs_present() {
return Err(Error::Codec(anyhow::anyhow!(
"NVIDIA driver libraries not found (libcuda / libnvidia-encode); NVENC unavailable"
)));
}
let codec_guid = codec_guid(config.codec);
let cuda = CudaContext::new(0).map_err(|e| Error::Codec(anyhow::anyhow!("CUDA init: {e:?}")))?;
let encoder = Encoder::initialize_with_cuda(cuda.clone())
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC init: {e}")))?;
let mut preset = encoder
.get_preset_config(
codec_guid,
NV_ENC_PRESET_P4_GUID,
NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_LOW_LATENCY,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC preset config: {e}")))?;
let cfg = &mut preset.presetCfg;
cfg.gopLength = config.gop;
cfg.frameIntervalP = 1; cfg.rcParams.rateControlMode = NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
cfg.rcParams.averageBitRate = config.resolved_bitrate().min(u32::MAX as u64) as u32;
unsafe {
match config.codec {
Codec::H264 => {
cfg.encodeCodecConfig.h264Config.set_repeatSPSPPS(1);
cfg.encodeCodecConfig.h264Config.idrPeriod = config.gop;
}
Codec::H265 => {
cfg.encodeCodecConfig.hevcConfig.set_repeatSPSPPS(1);
cfg.encodeCodecConfig.hevcConfig.idrPeriod = config.gop;
}
}
}
let mut init = EncoderInitParams::new(codec_guid, config.width, config.height);
init.preset_guid(NV_ENC_PRESET_P4_GUID)
.tuning_info(NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_LOW_LATENCY)
.framerate(config.framerate, 1)
.enable_picture_type_decision()
.encode_config(cfg);
let session = encoder
.start_session(NV_ENC_BUFFER_FORMAT::NV_ENC_BUFFER_FORMAT_NV12, init)
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC start session: {e}")))?;
tracing::info!(
encoder = NAME,
codec = ?config.codec,
width = config.width,
height = config.height,
"opened encoder"
);
Ok(Box::new(Self {
session,
_cuda: cuda,
timestamp: 0,
}))
}
}
impl Backend for Nvenc {
fn encode(&mut self, frame: &Frame, keyframe: bool) -> Result<Vec<Bytes>, Error> {
let mut output = self
.session
.create_output_bitstream()
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC output bitstream: {e}")))?;
let params = moq_nvenc::EncodePictureParams {
input_timestamp: self.timestamp,
force_idr: keyframe,
..Default::default()
};
self.timestamp += 1;
let data = match frame {
#[cfg(feature = "nvdec")]
Frame::Cuda(cuda) => {
let mut resource = self
.session
.register_generic_resource(
(),
NV_ENC_INPUT_RESOURCE_TYPE::NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR,
cuda.device_ptr() as *mut std::ffi::c_void,
cuda.pitch,
)
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC register CUDA frame: {e}")))?;
self.session
.encode_picture(&mut resource, &mut output, params)
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC encode: {e}")))?;
drain_output(&mut output)?
}
frame => {
let mut input = self
.session
.create_input_buffer()
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC input buffer: {e}")))?;
let i420 = frame.to_i420()?;
let (w, h) = (i420.width as usize, i420.height as usize);
let mut uv = vec![0u8; w * h / 2];
interleave_uv(i420.u(), i420.v(), &mut uv);
let mut lock = input
.lock()
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC lock input: {e}")))?;
let pitch = lock.pitch() as usize;
unsafe {
lock.write_rows(0, pitch, i420.y(), w, h);
lock.write_rows(pitch * h, pitch, &uv, w, h / 2);
}
drop(lock);
self.session
.encode_picture(&mut input, &mut output, params)
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC encode: {e}")))?;
drain_output(&mut output)?
}
};
Ok(if data.is_empty() {
Vec::new()
} else {
vec![Bytes::from(data)]
})
}
fn finish(&mut self) -> Result<Vec<Bytes>, Error> {
Ok(Vec::new())
}
fn set_bitrate(&mut self, bitrate: u64) -> Result<(), Error> {
self.session
.reconfigure(bitrate.min(u32::MAX as u64) as u32)
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC set bitrate to {bitrate}: {e}")))
}
fn name(&self) -> &str {
NAME
}
}
fn drain_output(output: &mut moq_nvenc::Bitstream) -> Result<Vec<u8>, Error> {
Ok(output
.lock()
.map_err(|e| Error::Codec(anyhow::anyhow!("NVENC lock output: {e}")))?
.data()
.to_vec())
}
fn driver_libs_present() -> bool {
const CUDA: &[&str] = &["libcuda.so.1", "libcuda.so"];
const NVENC: &[&str] = &["libnvidia-encode.so.1", "libnvidia-encode.so"];
let loadable = |names: &[&str]| {
names
.iter()
.any(|name| unsafe { libloading::Library::new(*name) }.is_ok())
};
loadable(CUDA) && loadable(NVENC)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encode::Config;
#[test]
fn missing_driver_errors_instead_of_panicking() {
if driver_libs_present() {
return; }
let config = Config::new(1920, 1080, 30);
assert!(Nvenc::open(&config).is_err());
}
fn gray_rgba(width: u32, height: u32) -> Vec<u8> {
vec![0x80u8; width as usize * height as usize * 4]
}
fn h264_nal_types(annexb: &[u8]) -> Vec<u8> {
let mut types = Vec::new();
let mut i = 0;
while i + 3 < annexb.len() {
if annexb[i..i + 3] == [0, 0, 1] {
types.push(annexb[i + 3] & 0x1f);
i += 3;
} else {
i += 1;
}
}
types
}
fn hevc_nal_types(annexb: &[u8]) -> Vec<u8> {
let mut types = Vec::new();
let mut i = 0;
while i + 3 < annexb.len() {
if annexb[i..i + 3] == [0, 0, 1] {
types.push((annexb[i + 3] >> 1) & 0x3f);
i += 3;
} else {
i += 1;
}
}
types
}
#[test]
fn nvenc_h264_keyframes_carry_param_sets() {
if !driver_libs_present() {
return;
}
let config = crate::encode::Config {
kind: crate::encode::Kind::Named(NAME.into()),
..crate::encode::Config::new(320, 240, 30)
};
let Ok(mut encoder) = crate::encode::Encoder::new(&config) else {
return;
};
assert_eq!(encoder.name(), NAME);
let frame = gray_rgba(320, 240);
let mut first = Vec::new();
let mut forced = Vec::new();
for i in 0..10u32 {
let keyframe = i == 0 || i == 5;
let packets = encoder
.encode_rgba(&frame, crate::Size::new(320, 240), keyframe)
.unwrap();
let joined: Vec<u8> = packets.iter().flatten().copied().collect();
if i == 0 {
first = joined;
} else if i == 5 {
forced = joined;
}
}
let types = h264_nal_types(&first);
assert!(types.contains(&7), "no SPS in first IDR: {types:?}");
assert!(types.contains(&8), "no PPS in first IDR: {types:?}");
assert!(types.contains(&5), "first packet is not an IDR: {types:?}");
let types = h264_nal_types(&forced);
assert!(types.contains(&5), "forced keyframe is not an IDR: {types:?}");
assert!(types.contains(&7), "forced IDR is missing inline SPS: {types:?}");
assert!(types.contains(&8), "forced IDR is missing inline PPS: {types:?}");
}
#[test]
fn nvenc_h265_keyframes_carry_param_sets() {
if !driver_libs_present() {
return;
}
let config = crate::encode::Config {
codec: crate::encode::Codec::H265,
kind: crate::encode::Kind::Named(NAME.into()),
..crate::encode::Config::new(320, 240, 30)
};
let Ok(mut encoder) = crate::encode::Encoder::new(&config) else {
return;
};
assert_eq!(encoder.name(), NAME);
assert_eq!(encoder.codec(), crate::encode::Codec::H265);
let frame = gray_rgba(320, 240);
let mut first = Vec::new();
let mut forced = Vec::new();
for i in 0..10u32 {
let keyframe = i == 0 || i == 5;
let packets = encoder
.encode_rgba(&frame, crate::Size::new(320, 240), keyframe)
.unwrap();
let joined: Vec<u8> = packets.iter().flatten().copied().collect();
if i == 0 {
first = joined;
} else if i == 5 {
forced = joined;
}
}
let is_irap = |t: &u8| (16..=23).contains(t);
let types = hevc_nal_types(&first);
assert!(types.contains(&32), "no VPS in first IRAP: {types:?}");
assert!(types.contains(&33), "no SPS in first IRAP: {types:?}");
assert!(types.contains(&34), "no PPS in first IRAP: {types:?}");
assert!(types.iter().any(is_irap), "first packet is not an IRAP: {types:?}");
let types = hevc_nal_types(&forced);
assert!(types.iter().any(is_irap), "forced keyframe is not an IRAP: {types:?}");
assert!(types.contains(&32), "forced IRAP is missing inline VPS: {types:?}");
assert!(types.contains(&33), "forced IRAP is missing inline SPS: {types:?}");
assert!(types.contains(&34), "forced IRAP is missing inline PPS: {types:?}");
}
#[test]
fn nvenc_h264_periodic_idr_at_gop() {
if !driver_libs_present() {
return;
}
let mut config = crate::encode::Config::new(320, 240, 30);
config.kind = crate::encode::Kind::Named(NAME.into());
config.gop = 3;
let Ok(mut encoder) = crate::encode::Encoder::new(&config) else {
return;
};
let frame = gray_rgba(320, 240);
let mut idr_frames = Vec::new();
for i in 0..7u32 {
let packets = encoder.encode_rgba(&frame, crate::Size::new(320, 240), false).unwrap();
let joined: Vec<u8> = packets.iter().flatten().copied().collect();
let types = h264_nal_types(&joined);
if types.contains(&5) {
assert!(types.contains(&7), "periodic IDR at frame {i} missing SPS: {types:?}");
assert!(types.contains(&8), "periodic IDR at frame {i} missing PPS: {types:?}");
idr_frames.push(i);
}
}
assert_eq!(idr_frames, vec![0, 3, 6], "IDRs not at the expected GOP boundaries");
}
fn gradient_rgba(width: u32, height: u32) -> Vec<u8> {
let (w, h) = (width as usize, height as usize);
let mut buf = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let i = (y * w + x) * 4;
buf[i] = (x * 255 / w) as u8;
buf[i + 1] = (y * 255 / h) as u8;
buf[i + 2] = ((x + y) * 255 / (w + h)) as u8;
buf[i + 3] = 255;
}
}
buf
}
#[test]
fn nvenc_h264_pitched_write_roundtrips() {
if !driver_libs_present() {
return;
}
let (w, h) = (300u32, 240u32);
let config = crate::encode::Config {
kind: crate::encode::Kind::Named(NAME.into()),
..crate::encode::Config::new(w, h, 30)
};
let Ok(mut encoder) = crate::encode::Encoder::new(&config) else {
return;
};
let rgba = gradient_rgba(w, h);
let expected = crate::frame::I420::from_rgba(&rgba, w * 4, w, h).unwrap();
let decode_config = crate::decode::Config {
kind: crate::decode::Kind::Software,
..crate::decode::Config::new()
};
let mut decoder = crate::decode::backend::open(crate::decode::backend::Codec::H264, &decode_config).unwrap();
let mut decoded = None;
for i in 0..10u64 {
let timestamp = moq_net::Timestamp::from_micros(i * 33_333).unwrap();
for packet in encoder.encode_rgba(&rgba, crate::Size::new(w, h), i == 0).unwrap() {
for out in decoder.decode(packet, timestamp, i == 0).unwrap() {
decoded = Some(out.frame.to_i420().unwrap().into_owned());
}
}
}
let decoded = decoded.expect("decoder produced at least one frame");
let mae =
|a: &[u8], b: &[u8]| a.iter().zip(b).map(|(x, y)| x.abs_diff(*y) as u64).sum::<u64>() / a.len() as u64;
assert!(mae(decoded.y(), expected.y()) < 8, "Y plane corrupt (pitch?)");
assert!(mae(decoded.u(), expected.u()) < 8, "U plane corrupt (pitch?)");
assert!(mae(decoded.v(), expected.v()) < 8, "V plane corrupt (pitch?)");
}
}