NetDesk 1.5.0 — source release
This commit is contained in:
commit
21dea33089
227 files changed
+80714
No files matched your search
@@ -0,0 +1,287 @@
|
||||
//! 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()))
|
||||
}
|
||||
Reference in new issue
Block a user