NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 00:47:49 +00:00
commit 21dea33089
227 files changed
+80714

No files matched your search

+287
View File
@@ -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()))
}