scythe/src/encoder.rs

1063 lines
44 KiB
Rust

use ffmpeg_next::ffi::*;
use std::ptr;
use std::collections::HashMap;
use crate::capture::Frame;
pub struct VaapiEncoder {
codec_ctx: *mut AVCodecContext,
hw_device_ctx: *mut AVBufferRef,
hw_frames_ctx: *mut AVBufferRef,
sws_ctx: *mut SwsContext,
staged_nv12: *mut AVFrame,
hw_frame: *mut AVFrame,
has_frame: bool,
pub force_idr: bool,
#[cfg(unix)]
dma_mmap_cache: HashMap<i32, (*mut libc::c_void, usize)>,
#[cfg(unix)]
direct_drm_unsupported: bool,
}
impl VaapiEncoder {
pub fn force_keyframe(&mut self) {
self.force_idr = true;
}
pub fn new(width: u32, height: u32) -> Result<Self, String> {
Self::new_with_params(width, height, 18_000, 60, "h264")
}
pub fn new_with_bitrate(width: u32, height: u32, bitrate_kbps: u32) -> Result<Self, String> {
Self::new_with_params(width, height, bitrate_kbps, 60, "h264")
}
pub fn new_with_params(width: u32, height: u32, bitrate_kbps: u32, fps: u32, codec_pref: &str) -> Result<Self, String> {
Self::new_with_options(width, height, bitrate_kbps, fps, codec_pref, "vbr", "limited", false)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_options(
width: u32,
height: u32,
bitrate_kbps: u32,
fps: u32,
codec_pref: &str,
quality_mode: &str,
color_range: &str,
low_power: bool,
) -> Result<Self, String> {
let fps = fps.clamp(20, 144) as i32;
let is_full = color_range.to_lowercase() == "full";
let is_cbr = quality_mode.to_lowercase() == "cbr";
unsafe {
let codec_candidates: &[&std::ffi::CStr] = match codec_pref.to_lowercase().as_str() {
"hevc" | "h265" => &[c"hevc_vaapi", c"h264_vaapi"],
"av1" => &[c"av1_vaapi", c"h264_vaapi"],
_ => &[c"h264_vaapi"],
};
let mut codec: *const AVCodec = ptr::null();
for cand in codec_candidates {
let c = avcodec_find_encoder_by_name(cand.as_ptr());
if !c.is_null() {
codec = c;
break;
}
}
if codec.is_null() {
return Err("No compatible VAAPI hardware encoder found".into());
}
let codec_ctx = avcodec_alloc_context3(codec);
if codec_ctx.is_null() {
return Err("Failed to allocate codec context".into());
}
(*codec_ctx).width = width as i32;
(*codec_ctx).height = height as i32;
(*codec_ctx).time_base = AVRational { num: 1, den: fps };
(*codec_ctx).framerate = AVRational { num: fps, den: 1 };
(*codec_ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_VAAPI;
(*codec_ctx).gop_size = fps; // Emit IDR keyframe every second
(*codec_ctx).max_b_frames = 0; // Zero latency, no frame reordering
// High Quality & Clean VBR Rate Control matching GPU Screen Recorder standards
let rate = (bitrate_kbps as i64) * 1000;
(*codec_ctx).bit_rate = rate;
if is_cbr {
(*codec_ctx).rc_max_rate = rate;
(*codec_ctx).rc_buffer_size = rate as i32;
let _ = av_opt_set((*codec_ctx).priv_data, c"rc_mode".as_ptr(), c"CBR".as_ptr(), 0);
} else {
(*codec_ctx).rc_max_rate = rate * 2; // Allow 2x burst for high-motion gaming scenes
(*codec_ctx).rc_buffer_size = (rate * 2) as i32; // 2 seconds rate control window
let _ = av_opt_set((*codec_ctx).priv_data, c"rc_mode".as_ptr(), c"VBR".as_ptr(), 0);
}
(*codec_ctx).qmin = 1; // Allow high-fidelity crisp detail
(*codec_ctx).qmax = 32;
(*codec_ctx).profile = match codec_pref.to_lowercase().as_str() {
"hevc" | "h265" => FF_PROFILE_HEVC_MAIN,
"av1" => FF_PROFILE_UNKNOWN,
_ => FF_PROFILE_H264_HIGH,
};
(*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int;
let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), 4, 0);
if low_power {
let _ = av_opt_set_int((*codec_ctx).priv_data, c"low_power".as_ptr(), 1, 0);
}
// Rec.709 Color Space & Color Range
(*codec_ctx).colorspace = AVColorSpace::AVCOL_SPC_BT709;
(*codec_ctx).color_primaries = AVColorPrimaries::AVCOL_PRI_BT709;
(*codec_ctx).color_trc = AVColorTransferCharacteristic::AVCOL_TRC_BT709;
(*codec_ctx).color_range = if is_full { AVColorRange::AVCOL_RANGE_JPEG } else { AVColorRange::AVCOL_RANGE_MPEG };
let mut hw_device_ctx: *mut AVBufferRef = ptr::null_mut();
let ret = av_hwdevice_ctx_create(
&mut hw_device_ctx,
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
ptr::null(),
ptr::null_mut(),
0,
);
if ret < 0 {
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to create VAAPI hardware device".into());
}
let hw_frames_ref = av_hwframe_ctx_alloc(hw_device_ctx);
if hw_frames_ref.is_null() {
av_buffer_unref(&mut hw_device_ctx);
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to allocate hw frames context".into());
}
let frames_ctx = (*hw_frames_ref).data as *mut AVHWFramesContext;
(*frames_ctx).format = AVPixelFormat::AV_PIX_FMT_VAAPI;
(*frames_ctx).sw_format = AVPixelFormat::AV_PIX_FMT_NV12;
(*frames_ctx).width = width as i32;
(*frames_ctx).height = height as i32;
(*frames_ctx).initial_pool_size = 20;
let ret = av_hwframe_ctx_init(hw_frames_ref);
if ret < 0 {
av_buffer_unref(&mut (hw_frames_ref as *mut _));
av_buffer_unref(&mut hw_device_ctx);
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to init hw frames context".into());
}
(*codec_ctx).hw_frames_ctx = av_buffer_ref(hw_frames_ref);
let ret = avcodec_open2(codec_ctx, codec, ptr::null_mut());
if ret < 0 {
av_buffer_unref(&mut (hw_frames_ref as *mut _));
av_buffer_unref(&mut hw_device_ctx);
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to open h264_vaapi encoder".into());
}
let staged_nv12 = av_frame_alloc();
(*staged_nv12).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*staged_nv12).width = width as i32;
(*staged_nv12).height = height as i32;
(*staged_nv12).colorspace = AVColorSpace::AVCOL_SPC_BT709;
(*staged_nv12).color_primaries = AVColorPrimaries::AVCOL_PRI_BT709;
(*staged_nv12).color_trc = AVColorTransferCharacteristic::AVCOL_TRC_BT709;
(*staged_nv12).color_range = AVColorRange::AVCOL_RANGE_MPEG;
av_frame_get_buffer(staged_nv12, 32);
let hw_frame = av_frame_alloc();
(*hw_frame).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
(*hw_frame).colorspace = AVColorSpace::AVCOL_SPC_BT709;
(*hw_frame).color_primaries = AVColorPrimaries::AVCOL_PRI_BT709;
(*hw_frame).color_trc = AVColorTransferCharacteristic::AVCOL_TRC_BT709;
(*hw_frame).color_range = if is_full { AVColorRange::AVCOL_RANGE_JPEG } else { AVColorRange::AVCOL_RANGE_MPEG };
let sws_ctx = sws_getContext(
width as i32,
height as i32,
AVPixelFormat::AV_PIX_FMT_BGRA,
width as i32,
height as i32,
AVPixelFormat::AV_PIX_FMT_NV12,
// SWS_FAST_BILINEAR (1) enables AVX2/SSSE3 SIMD assembly for sub-3ms color conversion
1,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
);
if !sws_ctx.is_null() {
let coeffs = sws_getCoefficients(SWS_CS_ITU709);
if !coeffs.is_null() {
let dst_range = if is_full { 1 } else { 0 };
sws_setColorspaceDetails(
sws_ctx,
coeffs,
1, // srcRange: 1 (sRGB full range 0-255)
coeffs,
dst_range,
0,
1 << 16,
1 << 16,
);
}
}
Ok(Self {
codec_ctx,
hw_device_ctx,
hw_frames_ctx: hw_frames_ref,
sws_ctx,
staged_nv12,
hw_frame,
has_frame: false,
force_idr: false,
#[cfg(unix)]
dma_mmap_cache: HashMap::new(),
#[cfg(unix)]
direct_drm_unsupported: false,
})
}
}
unsafe fn send_staged_frame(&mut self, packets: &mut Vec<crate::ring::Packet>, pts: i64) -> Result<(), String> {
unsafe {
av_frame_unref(self.hw_frame);
let ret = av_hwframe_get_buffer(self.hw_frames_ctx, self.hw_frame, 0);
if ret < 0 {
return Err("Failed to get hw frame buffer".into());
}
let transfer_ret = av_hwframe_transfer_data(self.hw_frame, self.staged_nv12, 0);
if transfer_ret < 0 {
av_frame_unref(self.hw_frame);
return Err("Failed to transfer staged nv12 data to hw frame".into());
}
(*self.hw_frame).pts = pts;
if self.force_idr {
(*self.hw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
self.force_idr = false;
} else {
(*self.hw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
}
if avcodec_send_frame(self.codec_ctx, self.hw_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
}
av_frame_unref(self.hw_frame);
Ok(())
}
}
pub fn codec_ctx(&self) -> *mut AVCodecContext {
self.codec_ctx
}
pub fn encode_frame(&mut self, frame: &Frame, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
unsafe {
match frame {
Frame::Raw { width: _, height, stride: in_stride, data } => {
let src_slices: [*const u8; 4] = [data.as_ptr(), ptr::null(), ptr::null(), ptr::null()];
let src_strides: [i32; 4] = [*in_stride as i32, 0, 0, 0];
sws_scale(
self.sws_ctx,
src_slices.as_ptr(),
src_strides.as_ptr(),
0,
*height as i32,
(*self.staged_nv12).data.as_ptr(),
(*self.staged_nv12).linesize.as_ptr(),
);
self.has_frame = true;
self.send_staged_frame(&mut packets, pts)?;
}
Frame::DmaBuf { width: dma_width, height: dma_height, format, modifier, fd, stride, offset } => {
#[allow(unused_variables)]
let mmap_size = (*offset as usize) + (*stride as usize) * (*dma_height as usize);
let mut mapped_direct = false;
#[cfg(unix)]
if !self.direct_drm_unsupported {
let desc = av_malloc(std::mem::size_of::<AVDRMFrameDescriptor>()) as *mut AVDRMFrameDescriptor;
if !desc.is_null() {
*desc = AVDRMFrameDescriptor {
nb_objects: 1,
objects: [AVDRMObjectDescriptor {
fd: *fd,
size: mmap_size,
format_modifier: *modifier,
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
nb_layers: 1,
layers: [AVDRMLayerDescriptor {
format: *format,
nb_planes: 1,
planes: [AVDRMPlaneDescriptor {
object_index: 0,
offset: *offset as isize,
pitch: *stride as isize,
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
}, std::mem::zeroed(), std::mem::zeroed(), std::mem::zeroed()],
};
let mut drm_frame = av_frame_alloc();
(*drm_frame).format = AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
(*drm_frame).width = *dma_width as i32;
(*drm_frame).height = *dma_height as i32;
(*drm_frame).data[0] = desc as *mut u8;
(*drm_frame).buf[0] = av_buffer_create(
desc as *mut u8,
std::mem::size_of::<AVDRMFrameDescriptor>(),
None,
ptr::null_mut(),
0,
);
let mut direct_hw = av_frame_alloc();
(*direct_hw).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
(*direct_hw).hw_frames_ctx = av_buffer_ref(self.hw_frames_ctx);
if av_hwframe_map(direct_hw, drm_frame, 0) >= 0 {
(*direct_hw).pts = pts;
if avcodec_send_frame(self.codec_ctx, direct_hw) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
mapped_direct = true;
self.has_frame = true;
}
} else {
self.direct_drm_unsupported = true;
}
av_frame_free(&mut direct_hw);
av_frame_free(&mut drm_frame);
}
}
if !mapped_direct {
#[cfg(unix)]
{
let ptr = match self.dma_mmap_cache.get(fd) {
Some(&(p, _)) => p,
None => {
let p = libc::mmap(
ptr::null_mut(),
mmap_size,
libc::PROT_READ,
libc::MAP_SHARED,
*fd,
0,
);
if p != libc::MAP_FAILED {
if self.dma_mmap_cache.len() >= 16
&& let Some(old_fd) = self.dma_mmap_cache.keys().next().copied()
&& let Some((old_p, old_size)) = self.dma_mmap_cache.remove(&old_fd) {
libc::munmap(old_p, old_size);
}
self.dma_mmap_cache.insert(*fd, (p, mmap_size));
p
} else {
libc::MAP_FAILED
}
}
};
if ptr != libc::MAP_FAILED {
let frame_ptr = (ptr as *const u8).add(*offset as usize);
let src_slices: [*const u8; 4] = [frame_ptr, ptr::null(), ptr::null(), ptr::null()];
let src_strides: [i32; 4] = [*stride as i32, 0, 0, 0];
sws_scale(
self.sws_ctx,
src_slices.as_ptr(),
src_strides.as_ptr(),
0,
*dma_height as i32,
(*self.staged_nv12).data.as_ptr(),
(*self.staged_nv12).linesize.as_ptr(),
);
self.has_frame = true;
self.send_staged_frame(&mut packets, pts)?;
}
}
}
}
#[cfg(target_os = "windows")]
Frame::D3D11Texture { handle, .. } => {
println!("D3D11: Mock encoding D3D11 texture (Windows zero-copy). Handle: {}", handle);
}
}
}
Ok(packets)
}
pub fn encode_cached_frame(&mut self, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
if !self.has_frame {
return Err("No cached frame available in encoder".into());
}
unsafe {
self.send_staged_frame(&mut packets, pts)?;
}
Ok(packets)
}
}
impl Drop for VaapiEncoder {
fn drop(&mut self) {
unsafe {
#[cfg(unix)]
for (_, (p, size)) in self.dma_mmap_cache.drain() {
if !p.is_null() && p != libc::MAP_FAILED {
libc::munmap(p, size);
}
}
if !self.staged_nv12.is_null() {
av_frame_free(&mut self.staged_nv12);
}
if !self.hw_frame.is_null() {
av_frame_free(&mut self.hw_frame);
}
if !self.sws_ctx.is_null() {
sws_freeContext(self.sws_ctx);
self.sws_ctx = ptr::null_mut();
}
if !self.codec_ctx.is_null() {
avcodec_free_context(&mut self.codec_ctx);
}
if !self.hw_device_ctx.is_null() {
av_buffer_unref(&mut self.hw_device_ctx);
}
if !self.hw_frames_ctx.is_null() {
av_buffer_unref(&mut self.hw_frames_ctx);
}
}
}
}
#[cfg(target_os = "windows")]
pub struct WindowsHwEncoder {
codec_ctx: *mut AVCodecContext,
encoder_name: String,
sws_ctx: *mut SwsContext,
sw_frame: *mut AVFrame,
pub has_frame: bool,
pub force_idr: bool,
}
#[cfg(target_os = "windows")]
impl WindowsHwEncoder {
pub fn force_keyframe(&mut self) {
self.force_idr = true;
}
pub fn new(width: u32, height: u32) -> Result<Self, String> {
Self::new_with_params(width, height, 20_000, 60, "h264")
}
pub fn new_with_params(width: u32, height: u32, bitrate_kbps: u32, fps: u32, codec_pref: &str) -> Result<Self, String> {
Self::new_with_options(width, height, bitrate_kbps, fps, codec_pref, "vbr", "limited", false)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_options(
width: u32,
height: u32,
bitrate_kbps: u32,
fps: u32,
codec_pref: &str,
quality_mode: &str,
color_range: &str,
low_power: bool,
) -> Result<Self, String> {
let fps = fps.clamp(20, 144) as i32;
let is_full = color_range.to_lowercase() == "full";
let is_cbr = quality_mode.to_lowercase() == "cbr";
unsafe {
let candidates: &[(&std::ffi::CStr, &str)] = match codec_pref.to_lowercase().as_str() {
"hevc" | "h265" => &[
(c"hevc_nvenc", "NVIDIA NVENC HEVC Hardware Encoder"),
(c"hevc_amf", "AMD AMF HEVC Hardware Encoder"),
(c"hevc_qsv", "Intel QuickSync HEVC Hardware Encoder"),
(c"libx265", "Software CPU HEVC Encoder"),
(c"h264_nvenc", "NVIDIA NVENC H.264 Fallback"),
(c"libx264", "Software CPU H.264 Fallback"),
],
"av1" => &[
(c"av1_nvenc", "NVIDIA NVENC AV1 Hardware Encoder"),
(c"av1_amf", "AMD AMF AV1 Hardware Encoder"),
(c"av1_qsv", "Intel QuickSync AV1 Hardware Encoder"),
(c"libsvtav1", "Software CPU SVT-AV1 Encoder"),
(c"h264_nvenc", "NVIDIA NVENC H.264 Fallback"),
(c"libx264", "Software CPU H.264 Fallback"),
],
_ => &[
(c"h264_nvenc", "NVIDIA NVENC Hardware Encoder"),
(c"h264_amf", "AMD AMF Hardware Encoder"),
(c"h264_qsv", "Intel QuickSync Hardware Encoder"),
(c"libx264", "Software CPU H.264 Encoder (Universal Fallback)"),
],
};
let mut opened_codec_ctx: *mut AVCodecContext = ptr::null_mut();
let mut selected_desc = String::new();
for (name, desc) in candidates {
let c = avcodec_find_encoder_by_name(name.as_ptr());
if c.is_null() {
continue;
}
let formats_to_try: &[AVPixelFormat] = if desc.contains("NVENC") || desc.contains("AMF") || desc.contains("QuickSync") {
&[AVPixelFormat::AV_PIX_FMT_NV12, AVPixelFormat::AV_PIX_FMT_YUV420P]
} else {
&[AVPixelFormat::AV_PIX_FMT_YUV420P, AVPixelFormat::AV_PIX_FMT_NV12]
};
for &pix_fmt in formats_to_try {
let ctx = avcodec_alloc_context3(c);
if ctx.is_null() {
continue;
}
(*ctx).width = width as i32;
(*ctx).height = height as i32;
(*ctx).time_base = AVRational { num: 1, den: fps };
(*ctx).framerate = AVRational { num: fps, den: 1 };
(*ctx).gop_size = fps;
(*ctx).max_b_frames = 0;
let rate = (bitrate_kbps as i64) * 1000;
(*ctx).bit_rate = rate;
if is_cbr {
(*ctx).rc_max_rate = rate;
(*ctx).rc_buffer_size = rate as i32;
} else {
(*ctx).rc_max_rate = rate * 2;
(*ctx).rc_buffer_size = (rate * 2) as i32;
}
(*ctx).qmin = 1;
(*ctx).qmax = 32;
let codec_id = (*c).id;
(*ctx).profile = if codec_id == AVCodecID::AV_CODEC_ID_HEVC {
FF_PROFILE_HEVC_MAIN
} else if codec_id == AVCodecID::AV_CODEC_ID_AV1 {
FF_PROFILE_UNKNOWN
} else {
FF_PROFILE_H264_HIGH
};
(*ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int;
(*ctx).pix_fmt = pix_fmt;
// ITU-R BT.709 Color Space & Color Range standard
(*ctx).colorspace = AVColorSpace::AVCOL_SPC_BT709;
(*ctx).color_primaries = AVColorPrimaries::AVCOL_PRI_BT709;
(*ctx).color_trc = AVColorTransferCharacteristic::AVCOL_TRC_BT709;
(*ctx).color_range = if is_full { AVColorRange::AVCOL_RANGE_JPEG } else { AVColorRange::AVCOL_RANGE_MPEG };
if desc.contains("NVENC") {
let preset = if low_power { c"p1".as_ptr() } else { c"p3".as_ptr() };
let rc = if is_cbr { c"cbr".as_ptr() } else { c"vbr".as_ptr() };
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), preset, 0);
let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"forced-idr".as_ptr(), c"1".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"rc".as_ptr(), rc, 0);
} else if desc.contains("AMF") {
let rc = if is_cbr { c"cbr".as_ptr() } else { c"vbr_latency".as_ptr() };
let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"rc".as_ptr(), rc, 0);
} else if desc.contains("QuickSync") {
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0);
} else {
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"ultrafast".as_ptr(), 0);
let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"zerolatency".as_ptr(), 0);
}
let ret = avcodec_open2(ctx, c, ptr::null_mut());
if ret >= 0 {
println!("Successfully initialized Windows video encoder: {} ({:?})", desc, pix_fmt);
opened_codec_ctx = ctx;
selected_desc = desc.to_string();
break;
} else {
let mut tmp = ctx;
avcodec_free_context(&mut tmp);
}
}
if !opened_codec_ctx.is_null() {
break;
}
}
if opened_codec_ctx.is_null() {
return Err("Failed to open any compatible video encoder on this system".into());
}
let codec_ctx = opened_codec_ctx;
let mut sw_frame = av_frame_alloc();
if sw_frame.is_null() {
avcodec_free_context(&mut opened_codec_ctx);
return Err("Failed to allocate frame for encoder".into());
}
(*sw_frame).format = (*codec_ctx).pix_fmt as i32;
(*sw_frame).width = width as i32;
(*sw_frame).height = height as i32;
(*sw_frame).colorspace = AVColorSpace::AVCOL_SPC_BT709;
(*sw_frame).color_primaries = AVColorPrimaries::AVCOL_PRI_BT709;
(*sw_frame).color_trc = AVColorTransferCharacteristic::AVCOL_TRC_BT709;
(*sw_frame).color_range = if is_full { AVColorRange::AVCOL_RANGE_JPEG } else { AVColorRange::AVCOL_RANGE_MPEG };
if av_frame_get_buffer(sw_frame, 32) < 0 {
av_frame_free(&mut sw_frame);
avcodec_free_context(&mut opened_codec_ctx);
return Err("Failed to allocate frame buffer for encoder".into());
}
let sws_ctx = sws_getContext(
width as i32,
height as i32,
AVPixelFormat::AV_PIX_FMT_BGRA,
width as i32,
height as i32,
(*codec_ctx).pix_fmt,
1, // SWS_FAST_BILINEAR for AVX2/SSSE3 SIMD acceleration
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
);
if sws_ctx.is_null() {
av_frame_free(&mut sw_frame);
avcodec_free_context(&mut opened_codec_ctx);
return Err("Failed to initialize color space conversion context".into());
}
let coeffs = sws_getCoefficients(SWS_CS_ITU709);
if !coeffs.is_null() {
let dst_range = if is_full { 1 } else { 0 };
sws_setColorspaceDetails(
sws_ctx,
coeffs,
1, // srcRange: 1 (sRGB full range 0-255)
coeffs,
dst_range,
0,
1 << 16,
1 << 16,
);
}
Ok(Self {
codec_ctx,
encoder_name: selected_desc,
sws_ctx,
sw_frame,
has_frame: false,
force_idr: false,
})
}
}
pub fn codec_ctx(&self) -> *mut AVCodecContext {
self.codec_ctx
}
pub fn encode_frame(&mut self, frame: &Frame, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
unsafe {
match frame {
Frame::Raw { width: _, height, stride, data } => {
if av_frame_make_writable(self.sw_frame) < 0 {
let _ = av_frame_get_buffer(self.sw_frame, 32);
}
if !self.sws_ctx.is_null() {
let src_data = [data.as_ptr(), ptr::null(), ptr::null(), ptr::null()];
let src_linesize = [*stride as i32, 0, 0, 0];
sws_scale(
self.sws_ctx,
src_data.as_ptr(),
src_linesize.as_ptr(),
0,
*height as i32,
(*self.sw_frame).data.as_mut_ptr(),
(*self.sw_frame).linesize.as_mut_ptr(),
);
}
(*self.sw_frame).pts = pts;
if self.force_idr {
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
self.force_idr = false;
} else {
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
}
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
self.has_frame = true;
}
}
_ => {}
}
}
Ok(packets)
}
#[cfg(target_os = "windows")]
pub fn encode_cached_frame(&mut self, pts: i64) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
if !self.has_frame {
return Err("No cached frame available in encoder".into());
}
unsafe {
if self.sw_frame.is_null() {
return Ok(packets);
}
(*self.sw_frame).pts = pts;
if self.force_idr {
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_I;
self.force_idr = false;
} else {
(*self.sw_frame).pict_type = AVPictureType::AV_PICTURE_TYPE_NONE;
}
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
}
}
Ok(packets)
}
}
#[cfg(target_os = "windows")]
impl Drop for WindowsHwEncoder {
fn drop(&mut self) {
unsafe {
if !self.sws_ctx.is_null() {
sws_freeContext(self.sws_ctx);
self.sws_ctx = ptr::null_mut();
}
if !self.sw_frame.is_null() {
av_frame_free(&mut self.sw_frame);
}
if !self.codec_ctx.is_null() {
avcodec_free_context(&mut self.codec_ctx);
}
}
}
}
#[cfg(target_os = "linux")]
pub type VideoEncoder = VaapiEncoder;
#[cfg(target_os = "windows")]
pub type VideoEncoder = WindowsHwEncoder;
pub struct AudioEncoder {
codec_ctx: *mut AVCodecContext,
swr_ctx: *mut SwrContext,
enc_frame: *mut AVFrame,
staging_out_frame: *mut AVFrame,
next_pts: i64,
frame_size: i32,
channels: i32,
fifo: *mut AVAudioFifo,
}
impl AudioEncoder {
pub fn new(sample_rate: i32, channels: i32) -> Result<Self, String> {
Self::new_with_codec(sample_rate, channels, "aac")
}
pub fn new_with_codec(sample_rate: i32, channels: i32, codec_pref: &str) -> Result<Self, String> {
unsafe {
let wants_opus = codec_pref.to_lowercase() == "opus";
let mut codec = if wants_opus {
avcodec_find_encoder(AVCodecID::AV_CODEC_ID_OPUS)
} else {
avcodec_find_encoder(AVCodecID::AV_CODEC_ID_AAC)
};
if codec.is_null() && wants_opus {
codec = avcodec_find_encoder_by_name(c"libopus".as_ptr());
if codec.is_null() {
codec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_AAC);
}
} else if codec.is_null() {
codec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_AAC);
}
if codec.is_null() {
return Err("No compatible audio encoder found (AAC/Opus)".into());
}
let codec_ctx = avcodec_alloc_context3(codec);
if codec_ctx.is_null() {
return Err("Failed to allocate audio codec context".into());
}
let sample_fmt = if (*codec).id == AVCodecID::AV_CODEC_ID_OPUS {
AVSampleFormat::AV_SAMPLE_FMT_FLT
} else {
AVSampleFormat::AV_SAMPLE_FMT_FLTP
};
(*codec_ctx).sample_rate = sample_rate;
(*codec_ctx).time_base = AVRational { num: 1, den: sample_rate };
(*codec_ctx).sample_fmt = sample_fmt;
(*codec_ctx).bit_rate = if (*codec).id == AVCodecID::AV_CODEC_ID_OPUS { 160_000 } else { 192_000 };
(*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int;
av_channel_layout_default(&mut (*codec_ctx).ch_layout, channels);
let ret = avcodec_open2(codec_ctx, codec, ptr::null_mut());
if ret < 0 {
avcodec_free_context(&mut (codec_ctx as *mut _));
return Err("Failed to open audio encoder".into());
}
let mut swr_ctx: *mut SwrContext = ptr::null_mut();
swr_alloc_set_opts2(
&mut swr_ctx,
&(*codec_ctx).ch_layout,
sample_fmt,
sample_rate,
&(*codec_ctx).ch_layout,
AVSampleFormat::AV_SAMPLE_FMT_FLT,
sample_rate,
0,
ptr::null_mut(),
);
swr_init(swr_ctx);
let frame_size = if (*codec_ctx).frame_size > 0 { (*codec_ctx).frame_size } else { 960 };
let fifo = av_audio_fifo_alloc(sample_fmt, channels, 1024 * 32);
let enc_frame = av_frame_alloc();
(*enc_frame).nb_samples = frame_size;
(*enc_frame).format = sample_fmt as i32;
(*enc_frame).ch_layout = (*codec_ctx).ch_layout;
(*enc_frame).sample_rate = sample_rate;
av_frame_get_buffer(enc_frame, 0);
let staging_out_frame = av_frame_alloc();
(*staging_out_frame).nb_samples = 8192;
(*staging_out_frame).format = sample_fmt as i32;
(*staging_out_frame).ch_layout = (*codec_ctx).ch_layout;
(*staging_out_frame).sample_rate = sample_rate;
av_frame_get_buffer(staging_out_frame, 0);
Ok(Self {
codec_ctx,
swr_ctx,
enc_frame,
staging_out_frame,
next_pts: 0,
frame_size,
channels,
fifo,
})
}
}
pub fn codec_ctx(&self) -> *mut AVCodecContext {
self.codec_ctx
}
pub fn encode_pcm(&mut self, data: &[f32]) -> Result<Vec<crate::ring::Packet>, String> {
let mut packets = Vec::new();
unsafe {
let nb_samples = data.len() as i32 / self.channels;
if nb_samples <= 0 {
return Ok(packets);
}
if (*self.staging_out_frame).nb_samples < nb_samples {
av_frame_unref(self.staging_out_frame);
(*self.staging_out_frame).nb_samples = nb_samples.max(8192);
(*self.staging_out_frame).format = AVSampleFormat::AV_SAMPLE_FMT_FLTP as i32;
(*self.staging_out_frame).ch_layout = (*self.codec_ctx).ch_layout;
(*self.staging_out_frame).sample_rate = (*self.codec_ctx).sample_rate;
av_frame_get_buffer(self.staging_out_frame, 0);
}
let in_ptr = [data.as_ptr() as *const u8, ptr::null()];
swr_convert(
self.swr_ctx,
(*self.staging_out_frame).data.as_mut_ptr(),
nb_samples,
in_ptr.as_ptr() as *mut _,
nb_samples,
);
av_audio_fifo_write(
self.fifo,
(*self.staging_out_frame).data.as_mut_ptr() as *mut *mut std::ffi::c_void,
nb_samples,
);
// Read exactly frame_size chunks from FIFO and encode
while av_audio_fifo_size(self.fifo) >= self.frame_size {
av_audio_fifo_read(
self.fifo,
(*self.enc_frame).data.as_mut_ptr() as *mut *mut std::ffi::c_void,
self.frame_size,
);
(*self.enc_frame).pts = self.next_pts;
self.next_pts += self.frame_size as i64;
if avcodec_send_frame(self.codec_ctx, self.enc_frame) >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
}
}
}
Ok(packets)
}
}
impl Drop for AudioEncoder {
fn drop(&mut self) {
unsafe {
if !self.staging_out_frame.is_null() {
av_frame_free(&mut self.staging_out_frame);
}
if !self.enc_frame.is_null() {
av_frame_free(&mut self.enc_frame);
}
if !self.fifo.is_null() {
av_audio_fifo_free(self.fifo);
self.fifo = ptr::null_mut();
}
if !self.swr_ctx.is_null() {
swr_free(&mut self.swr_ctx);
self.swr_ctx = ptr::null_mut();
}
if !self.codec_ctx.is_null() {
avcodec_free_context(&mut self.codec_ctx);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audio_encoder_global_header() {
let enc = AudioEncoder::new(48000, 2).expect("AudioEncoder creation");
unsafe {
let flags = (*enc.codec_ctx).flags;
assert!((flags & (AV_CODEC_FLAG_GLOBAL_HEADER as i32)) != 0, "Audio encoder must have GLOBAL_HEADER flag");
assert!((*enc.codec_ctx).extradata_size > 0, "Audio encoder extradata must be populated");
}
}
#[test]
fn test_opus_audio_encoder_initialization() {
let enc_res = AudioEncoder::new_with_codec(48000, 2, "opus");
assert!(enc_res.is_ok(), "Opus encoder initialization should succeed");
}
#[test]
fn test_audio_encode_sine_wave() {
let mut enc = AudioEncoder::new(48000, 2).expect("AudioEncoder creation");
// Generate 48000 stereo samples of 440Hz sine wave (1.0s)
let mut pcm = Vec::with_capacity(48000 * 2);
for i in 0..48000 {
let val = ((i as f32 * 440.0 * 2.0 * std::f32::consts::PI) / 48000.0).sin() * 0.8;
pcm.push(val); // L
pcm.push(val); // R
}
let packets = enc.encode_pcm(&pcm).expect("encode_pcm");
println!("Encoded {} AAC packets from sine wave", packets.len());
assert!(!packets.is_empty(), "Must produce AAC packets");
let total_size: usize = packets.iter().map(|p| unsafe { (*p.ptr).size as usize }).sum();
println!("Total AAC packet bytes: {}", total_size);
assert!(total_size > 1000, "AAC packets must not be empty or silence");
}
#[test]
fn test_vaapi_encoder_global_header() {
if let Ok(mut enc) = VaapiEncoder::new(1280, 720) {
unsafe {
let flags = (*enc.codec_ctx).flags;
assert!((flags & (AV_CODEC_FLAG_GLOBAL_HEADER as i32)) != 0, "Vaapi encoder must have GLOBAL_HEADER flag");
}
// Encode a blank BGRA frame
let blank_frame = Frame::Raw {
width: 1280,
height: 720,
stride: 1280 * 4,
data: std::sync::Arc::new(vec![0u8; 1280 * 720 * 4]),
};
let pkts = enc.encode_frame(&blank_frame, 0).expect("Encode frame");
println!("Encoded {} packets from first frame", pkts.len());
unsafe {
println!("VAAPI extradata_size after encode: {}", (*enc.codec_ctx).extradata_size);
}
}
}
#[test]
fn test_vaapi_encoder_60fps_throughput() {
if let Ok(mut enc) = VaapiEncoder::new_with_params(1920, 1080, 20_000, 60, "h264") {
let blank_frame = Frame::Raw {
width: 1920,
height: 1080,
stride: 1920 * 4,
data: std::sync::Arc::new(vec![128u8; 1920 * 1080 * 4]),
};
// Warm up
let _ = enc.encode_frame(&blank_frame, 0);
let iterations = 30;
let start = std::time::Instant::now();
for i in 1..=iterations {
let _ = enc.encode_frame(&blank_frame, i);
}
let elapsed = start.elapsed();
let avg_ms = elapsed.as_secs_f64() * 1000.0 / (iterations as f64);
println!("1080p60 VAAPI frame encode avg time: {:.2} ms per frame", avg_ms);
// Frame::Raw incurs software swscale + PCIe GPU upload; assert healthy hardware throughput under 35ms
assert!(avg_ms < 35.0, "Encoding time ({:.2}ms) must be under 35ms for software upload fallback", avg_ms);
}
}
}