288 lines
11 KiB
Rust
288 lines
11 KiB
Rust
//! 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<Self> {
|
|
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<Option<Frame>> {
|
|
// 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::<c_int, AVPixelFormat>(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<Option<Frame>> {
|
|
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<Frame> = 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()))
|
|
}
|