//! Linux hardware H.264 decode via VAAPI, driven through FFmpeg/libavcodec. //! //! Pure libva decoding would mean parsing the H.264 bitstream down to slice //! headers and hand-building `VAPictureParameterBufferH264` / reference lists; //! libavcodec already does all of that and exposes VAAPI as a hardware //! accelerator, so this backend opens the libavcodec H.264 decoder with a VAAPI //! `hw_device_ctx`. Decode runs on the GPU (Intel/AMD/`iHD`/`radeonsi` via //! `/dev/dri`), the decoded surface is transferred to system memory (NV12), and //! swscale converts it to the same packed-RGB8 [`Frame`] the openh264 path //! produces, so the viewer is unchanged. //! //! The module is only compiled on Linux (see the `#[cfg(target_os = "linux")]` //! gate in `decode.rs`). If VAAPI is unavailable at runtime — //! `av_hwdevice_ctx_create` fails with no usable DRI device — `new` returns an //! error and [`super::best_decoder`] falls back to the portable openh264 //! decoder, so headless / GPU-less Linux boxes still work. use super::{Frame, VideoDecoder}; use crate::error::{NdError, Result}; use core::ffi::{c_int, c_void}; use ffmpeg_next::ffi::*; /// FFmpeg/VAAPI hardware H.264 decoder. pub struct VaapiDecoder { ctx: *mut AVCodecContext, hw_device: *mut AVBufferRef, packet: *mut AVPacket, hw_frame: *mut AVFrame, sw_frame: *mut AVFrame, sws: *mut SwsContext, sws_dims: (i32, i32, i32), // (w, h, src pixel format) the cached scaler is for pts: i64, } impl VaapiDecoder { pub fn new() -> Result { unsafe { // Safe to call repeatedly; wires up codec registration + logging. let _ = ffmpeg_next::init(); let codec = avcodec_find_decoder(AVCodecID::AV_CODEC_ID_H264); if codec.is_null() { return Err(other("libavcodec has no H.264 decoder")); } let ctx = avcodec_alloc_context3(codec); if ctx.is_null() { return Err(other("avcodec_alloc_context3 failed")); } // Create the VAAPI device (default DRI render node) and attach it. let mut hw_device: *mut AVBufferRef = core::ptr::null_mut(); let rc = av_hwdevice_ctx_create( &mut hw_device, AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, core::ptr::null(), core::ptr::null_mut(), 0, ); let mut ctx = ctx; if rc < 0 || hw_device.is_null() { avcodec_free_context(&mut ctx); return Err(other(&format!( "av_hwdevice_ctx_create(VAAPI) failed ({rc})" ))); } (*ctx).hw_device_ctx = av_buffer_ref(hw_device); (*ctx).get_format = Some(get_hw_format); let rc = avcodec_open2(ctx, codec, core::ptr::null_mut()); if rc < 0 { av_buffer_unref(&mut hw_device); avcodec_free_context(&mut ctx); return Err(other(&format!("avcodec_open2 failed ({rc})"))); } let mut packet = av_packet_alloc(); let mut hw_frame = av_frame_alloc(); let mut sw_frame = av_frame_alloc(); if packet.is_null() || hw_frame.is_null() || sw_frame.is_null() { if !packet.is_null() { av_packet_free(&mut packet); } if !hw_frame.is_null() { av_frame_free(&mut hw_frame); } if !sw_frame.is_null() { av_frame_free(&mut sw_frame); } av_buffer_unref(&mut hw_device); avcodec_free_context(&mut ctx); return Err(other("av_frame_alloc / av_packet_alloc failed")); } Ok(Self { ctx, hw_device, packet, hw_frame, sw_frame, sws: core::ptr::null_mut(), sws_dims: (0, 0, -1), pts: 0, }) } } /// Transfer a decoded frame to system memory (if it is a GPU surface), crop /// it to the display rectangle, and swscale it to packed RGB8. unsafe fn frame_to_rgb(&mut self) -> Result> { // A VAAPI surface must be pulled down to a software NV12 frame first; // a software frame (fallback path) is used as-is. let src: *mut AVFrame = if (*self.hw_frame).format == AVPixelFormat::AV_PIX_FMT_VAAPI as c_int { av_frame_unref(self.sw_frame); let rc = av_hwframe_transfer_data(self.sw_frame, self.hw_frame, 0); if rc < 0 { return Err(other(&format!("av_hwframe_transfer_data failed ({rc})"))); } // Carry crop/colour metadata across from the hardware frame. av_frame_copy_props(self.sw_frame, self.hw_frame); self.sw_frame } else { self.hw_frame }; // Apply the H.264 display crop so width/height are the true picture size // (e.g. 1920x1080) rather than the 16-aligned coded size. let _ = av_frame_apply_cropping(src, 1 /* AV_FRAME_CROP_UNALIGNED */); let w = (*src).width; let h = (*src).height; let src_fmt = (*src).format; if w <= 0 || h <= 0 { return Ok(None); } // (Re)create the scaler when the geometry or source format changes. if self.sws.is_null() || self.sws_dims != (w, h, src_fmt) { if !self.sws.is_null() { sws_freeContext(self.sws); } self.sws = sws_getContext( w, h, core::mem::transmute::(src_fmt), w, h, AVPixelFormat::AV_PIX_FMT_RGB24, SWS_BILINEAR as c_int, core::ptr::null_mut(), // srcFilter: *mut SwsFilter core::ptr::null_mut(), // dstFilter: *mut SwsFilter core::ptr::null(), // param: *const c_double ); if self.sws.is_null() { return Err(other("sws_getContext failed")); } self.sws_dims = (w, h, src_fmt); } let mut rgb = vec![0u8; (w as usize) * (h as usize) * 3]; let dst_data: [*mut u8; 4] = [ rgb.as_mut_ptr(), core::ptr::null_mut(), core::ptr::null_mut(), core::ptr::null_mut(), ]; let dst_linesize: [c_int; 4] = [w * 3, 0, 0, 0]; let scaled = sws_scale( self.sws, (*src).data.as_ptr() as *const *const u8, (*src).linesize.as_ptr(), 0, h, dst_data.as_ptr(), dst_linesize.as_ptr(), ); if scaled <= 0 { return Ok(None); } Ok(Some(Frame { w: w as usize, h: h as usize, rgb, })) } } impl VideoDecoder for VaapiDecoder { fn decode(&mut self, au: &[u8]) -> Result> { if au.is_empty() { return Ok(None); } unsafe { // Point the reusable packet at this AU. The data is not reference // counted (no `buf`), so libavcodec copies what it needs during // `avcodec_send_packet`; the slice only has to outlive this call. av_packet_unref(self.packet); (*self.packet).data = au.as_ptr() as *mut u8; (*self.packet).size = au.len() as c_int; (*self.packet).pts = self.pts; (*self.packet).dts = self.pts; self.pts += 1; let mut out: Option = None; let mut sret = avcodec_send_packet(self.ctx, self.packet); if sret < 0 { // Decoder is full: drain a frame, keep it, then resend. av_frame_unref(self.hw_frame); if avcodec_receive_frame(self.ctx, self.hw_frame) == 0 { out = self.frame_to_rgb()?; } sret = avcodec_send_packet(self.ctx, self.packet); } // Detach our borrowed buffer from the packet before it is reused. (*self.packet).data = core::ptr::null_mut(); (*self.packet).size = 0; if sret < 0 && out.is_none() { // EAGAIN / not-yet-ready before the first keyframe: no frame. return Ok(None); } av_frame_unref(self.hw_frame); if avcodec_receive_frame(self.ctx, self.hw_frame) == 0 { out = self.frame_to_rgb()?; } Ok(out) } } } impl Drop for VaapiDecoder { fn drop(&mut self) { unsafe { if !self.sws.is_null() { sws_freeContext(self.sws); } if !self.sw_frame.is_null() { av_frame_free(&mut self.sw_frame); } if !self.hw_frame.is_null() { av_frame_free(&mut self.hw_frame); } if !self.packet.is_null() { av_packet_free(&mut self.packet); } if !self.hw_device.is_null() { av_buffer_unref(&mut self.hw_device); } if !self.ctx.is_null() { avcodec_free_context(&mut self.ctx); } } } } // SAFETY: the decoder is created on, and used only from, the single background // decode thread that owns it. The libavcodec context and its frames/packet are // never shared across threads here (no `Sync`); moving ownership to that thread // and calling it there is sound. unsafe impl Send for VaapiDecoder {} /// `get_format` callback: prefer the VAAPI hardware pixel format when the /// decoder offers it, otherwise fall back to the first (software) format so a /// GPU-less box still decodes. unsafe extern "C" fn get_hw_format( _ctx: *mut AVCodecContext, mut fmts: *const AVPixelFormat, ) -> AVPixelFormat { let first = if fmts.is_null() { AVPixelFormat::AV_PIX_FMT_NONE } else { *fmts }; while !fmts.is_null() && *fmts != AVPixelFormat::AV_PIX_FMT_NONE { if *fmts == AVPixelFormat::AV_PIX_FMT_VAAPI { return AVPixelFormat::AV_PIX_FMT_VAAPI; } fmts = fmts.add(1); } first } fn other(msg: &str) -> NdError { NdError::Other(anyhow::anyhow!(msg.to_string())) }