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, #[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::new_with_params(width, height, 18_000, 60, "h264") } pub fn new_with_bitrate(width: u32, height: u32, bitrate_kbps: u32) -> Result { 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::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 { 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, 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, 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::()) 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::(), 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, 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::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::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 { 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, 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, 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::new_with_codec(sample_rate, channels, "aac") } pub fn new_with_codec(sample_rate: i32, channels: i32, codec_pref: &str) -> Result { 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, 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); } } }