Lightningbeam/lightningbeam-ui/gpu-video-encoder/src/decoder.rs

218 lines
8.4 KiB
Rust

//! VAAPI hardware video decode → wgpu textures. The mirror of [`crate::encoder`]: the codec
//! decodes into a VAAPI NV12 surface, which is mapped to a DRM-PRIME DMA-BUF and imported as two
//! wgpu plane textures via [`crate::dmabuf::import_raw`] — the exact same path the encoder uses,
//! in the read direction. Stays GPU-resident: no CPU frame copy.
use crate::dmabuf::{self, ImportedNv12, Nv12DmaBuf};
use crate::vk_device::{self, DrmDevice};
use ffmpeg_sys_next as ff;
use std::ffi::CString;
use std::path::Path;
use std::ptr;
#[inline]
fn averror(e: i32) -> i32 {
-e
}
/// `get_format` callback: pick VAAPI surfaces so the decoder outputs hardware frames. With
/// `hw_device_ctx` set, FFmpeg auto-allocates the matching frames context.
unsafe extern "C" fn get_vaapi_format(
_ctx: *mut ff::AVCodecContext,
mut fmts: *const ff::AVPixelFormat,
) -> ff::AVPixelFormat {
while *fmts != ff::AVPixelFormat::AV_PIX_FMT_NONE {
if *fmts == ff::AVPixelFormat::AV_PIX_FMT_VAAPI {
return ff::AVPixelFormat::AV_PIX_FMT_VAAPI;
}
fmts = fmts.add(1);
}
ff::AVPixelFormat::AV_PIX_FMT_NONE
}
/// Hardware decoder for a single video file/stream. Frames come back as importable NV12 textures
/// on [`Self::device`].
pub struct VaapiDecoder {
drm: DrmDevice,
hw_device: *mut ff::AVBufferRef,
fmt: *mut ff::AVFormatContext,
dec: *mut ff::AVCodecContext,
pkt: *mut ff::AVPacket,
frame: *mut ff::AVFrame,
stream_index: i32,
flushing: bool,
}
// Owns its FFmpeg/Vulkan handles exclusively; only moved, never shared (same as the encoder).
unsafe impl Send for VaapiDecoder {}
impl VaapiDecoder {
/// Open `input_path` and set up VAAPI hardware decoding of its best video stream.
pub fn new(input_path: &Path) -> Result<Self, String> {
let drm = vk_device::create()?;
unsafe {
let mut hw_device = crate::vaapi::create_device()?;
let cleanup_hw = |hw: *mut ff::AVBufferRef| {
let mut h = hw;
ff::av_buffer_unref(&mut h);
};
let path_c = CString::new(input_path.to_string_lossy().as_ref()).unwrap();
let mut fmt: *mut ff::AVFormatContext = ptr::null_mut();
if ff::avformat_open_input(&mut fmt, path_c.as_ptr(), ptr::null_mut(), ptr::null_mut()) < 0 {
cleanup_hw(hw_device);
return Err(format!("avformat_open_input {input_path:?} failed"));
}
if ff::avformat_find_stream_info(fmt, ptr::null_mut()) < 0 {
ff::avformat_close_input(&mut fmt);
cleanup_hw(hw_device);
return Err("avformat_find_stream_info failed".into());
}
let mut decoder: *const ff::AVCodec = ptr::null();
let stream_index = ff::av_find_best_stream(
fmt,
ff::AVMediaType::AVMEDIA_TYPE_VIDEO,
-1,
-1,
&mut decoder,
0,
);
if stream_index < 0 || decoder.is_null() {
ff::avformat_close_input(&mut fmt);
cleanup_hw(hw_device);
return Err("no decodable video stream".into());
}
let dec = ff::avcodec_alloc_context3(decoder);
let stream = *(*fmt).streams.add(stream_index as usize);
if ff::avcodec_parameters_to_context(dec, (*stream).codecpar) < 0 {
ff::avcodec_free_context(&mut (dec as *mut _));
ff::avformat_close_input(&mut fmt);
cleanup_hw(hw_device);
return Err("avcodec_parameters_to_context failed".into());
}
(*dec).hw_device_ctx = ff::av_buffer_ref(hw_device);
(*dec).get_format = Some(get_vaapi_format);
if ff::avcodec_open2(dec, decoder, ptr::null_mut()) < 0 {
ff::avcodec_free_context(&mut (dec as *mut _));
ff::avformat_close_input(&mut fmt);
cleanup_hw(hw_device);
return Err("avcodec_open2 (vaapi decode) failed".into());
}
// `mut` only to satisfy the move into the struct; the binding above is consumed.
let _ = &mut hw_device;
Ok(Self {
drm,
hw_device,
fmt,
dec,
pkt: ff::av_packet_alloc(),
frame: ff::av_frame_alloc(),
stream_index,
flushing: false,
})
}
}
/// The wgpu device the decoded textures live on (the DMA-BUF-import device).
pub fn device(&self) -> &wgpu::Device {
&self.drm.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.drm.queue
}
/// Decode the next frame and import it as NV12 plane textures, or `Ok(None)` at end of stream.
pub fn next_frame(&mut self) -> Result<Option<ImportedNv12>, String> {
unsafe {
loop {
let r = ff::avcodec_receive_frame(self.dec, self.frame);
if r == 0 {
let imported = self.map_current();
ff::av_frame_unref(self.frame);
return imported.map(Some);
}
if r == ff::AVERROR_EOF {
return Ok(None);
}
if r != averror(libc::EAGAIN) {
return Err(format!("avcodec_receive_frame failed: {r}"));
}
if self.flushing {
return Ok(None); // already drained the flush
}
// Decoder wants more input: pump one packet (or signal EOF to flush).
let rp = ff::av_read_frame(self.fmt, self.pkt);
if rp < 0 {
self.flushing = true;
ff::avcodec_send_packet(self.dec, ptr::null());
continue;
}
if (*self.pkt).stream_index == self.stream_index {
let rs = ff::avcodec_send_packet(self.dec, self.pkt);
ff::av_packet_unref(self.pkt);
if rs < 0 && rs != averror(libc::EAGAIN) {
return Err(format!("avcodec_send_packet failed: {rs}"));
}
} else {
ff::av_packet_unref(self.pkt);
}
}
}
}
/// Map the just-decoded VAAPI surface (`self.frame`) to a DRM-PRIME DMA-BUF and import it.
unsafe fn map_current(&self) -> Result<ImportedNv12, String> {
let drm_f = ff::av_frame_alloc();
(*drm_f).format = ff::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
let flags = ff::AV_HWFRAME_MAP_DIRECT as i32 | ff::AV_HWFRAME_MAP_READ as i32;
if ff::av_hwframe_map(drm_f, self.frame, flags) < 0 {
ff::av_frame_free(&mut (drm_f as *mut _));
return Err("av_hwframe_map failed".into());
}
let desc = (*drm_f).data[0] as *const ff::AVDRMFrameDescriptor;
let obj = &(*desc).objects[0];
let width = (*self.frame).width as u32;
let height = (*self.frame).height as u32;
// NV12: Y then UV — either as two layers (one plane each) or one layer with two planes.
let (y_pl, uv_pl) = if (*desc).nb_layers >= 2 {
(&(*desc).layers[0].planes[0], &(*desc).layers[1].planes[0])
} else {
(&(*desc).layers[0].planes[0], &(*desc).layers[0].planes[1])
};
let buf = Nv12DmaBuf {
fd: obj.fd,
size: obj.size as u64,
modifier: obj.format_modifier,
width,
height,
y_offset: y_pl.offset as u64,
y_pitch: y_pl.pitch as u64,
uv_offset: uv_pl.offset as u64,
uv_pitch: uv_pl.pitch as u64,
ten_bit: false,
};
let imported = dmabuf::import_raw(&self.drm.device, &self.drm.adapter, &buf);
ff::av_frame_free(&mut (drm_f as *mut _)); // the fd was dup'd into Vulkan
imported
}
}
impl Drop for VaapiDecoder {
fn drop(&mut self) {
unsafe {
ff::av_frame_free(&mut (self.frame as *mut _));
ff::av_packet_free(&mut (self.pkt as *mut _));
ff::avcodec_free_context(&mut (self.dec as *mut _));
if !self.fmt.is_null() {
ff::avformat_close_input(&mut self.fmt);
}
ff::av_buffer_unref(&mut self.hw_device);
}
}
}