feat(avhw): integrate transform into VA-API filter graph
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
131
src/avhw.rs
131
src/avhw.rs
@@ -4,9 +4,11 @@ use std::ptr;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use ffmpeg_next as ff;
|
||||
use ffmpeg_next::ffi as ffi;
|
||||
use ffmpeg_next::ffi;
|
||||
use ffmpeg_next::packet::Mut as _;
|
||||
|
||||
use crate::transform::Transform;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AvHwDevCtx
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -69,12 +71,7 @@ pub struct AvHwFrameCtx {
|
||||
unsafe impl Send for AvHwFrameCtx {}
|
||||
|
||||
impl AvHwFrameCtx {
|
||||
fn new_inner(
|
||||
hw_dev: &AvHwDevCtx,
|
||||
w: u32,
|
||||
h: u32,
|
||||
sw_fmt: ff::format::Pixel,
|
||||
) -> Result<Self> {
|
||||
fn new_inner(hw_dev: &AvHwDevCtx, w: u32, h: u32, sw_fmt: ff::format::Pixel) -> Result<Self> {
|
||||
let mut p = unsafe { ffi::av_hwframe_ctx_alloc(hw_dev.as_ptr()) };
|
||||
if p.is_null() {
|
||||
bail!("av_hwframe_ctx_alloc returned null");
|
||||
@@ -159,24 +156,25 @@ impl EncState {
|
||||
output_path: &Path,
|
||||
width: u32,
|
||||
height: u32,
|
||||
enc_width: u32,
|
||||
enc_height: u32,
|
||||
bitrate: u64,
|
||||
gop_size: u32,
|
||||
fps: u32,
|
||||
transform: Transform,
|
||||
) -> Result<Self> {
|
||||
// 1. VAAPI device
|
||||
let hw_device_ctx = AvHwDevCtx::new_vaapi(drm_device)?;
|
||||
|
||||
// 2. Frame contexts (capture=XRGB/RGBZ, encode=NV12)
|
||||
let frames_rgb = AvHwFrameCtx::for_capture(
|
||||
&hw_device_ctx,
|
||||
width,
|
||||
height,
|
||||
ff::format::Pixel::RGBZ,
|
||||
)?;
|
||||
// frames_rgb uses original capture dimensions (matches raw framebuffer)
|
||||
// frames_yuv uses encoder dimensions (transposed for 90°/270° rotations)
|
||||
let frames_rgb =
|
||||
AvHwFrameCtx::for_capture(&hw_device_ctx, width, height, ff::format::Pixel::RGBZ)?;
|
||||
let frames_yuv = AvHwFrameCtx::for_encode(
|
||||
&hw_device_ctx,
|
||||
width,
|
||||
height,
|
||||
enc_width,
|
||||
enc_height,
|
||||
ff::format::Pixel::NV12,
|
||||
)?;
|
||||
|
||||
@@ -189,8 +187,8 @@ impl EncState {
|
||||
ctx.encoder().video()?
|
||||
};
|
||||
|
||||
enc.set_width(width);
|
||||
enc.set_height(height);
|
||||
enc.set_width(enc_width);
|
||||
enc.set_height(enc_height);
|
||||
enc.set_format(ff::format::Pixel::VAAPI);
|
||||
enc.set_bit_rate(bitrate as usize);
|
||||
enc.set_gop(gop_size);
|
||||
@@ -226,14 +224,22 @@ impl EncState {
|
||||
}
|
||||
|
||||
// 4. Open encoder. Video::open() returns Encoder(Video); .0 extracts the Video.
|
||||
let opened = enc.open().map_err(|e| {
|
||||
anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}")
|
||||
})?;
|
||||
let opened = enc
|
||||
.open()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}"))?;
|
||||
let enc_video = opened.0;
|
||||
|
||||
// 5. Filter graph (inline)
|
||||
let video_filter =
|
||||
build_filter_graph(&hw_device_ctx, &frames_rgb, width, height, fps)?;
|
||||
let video_filter = build_filter_graph(
|
||||
&hw_device_ctx,
|
||||
&frames_rgb,
|
||||
width,
|
||||
height,
|
||||
enc_width,
|
||||
enc_height,
|
||||
fps,
|
||||
transform,
|
||||
)?;
|
||||
|
||||
// 6. Muxer setup (strict order)
|
||||
let output_cstr = CString::new(output_path.to_str().unwrap())?;
|
||||
@@ -329,9 +335,9 @@ impl EncState {
|
||||
let mut filter_sink = filter_sink_ctx.sink();
|
||||
|
||||
// SAFETY: hw_frame is a valid VAAPI hardware frame from capture.
|
||||
filter_src.add(hw_frame).map_err(|e| {
|
||||
anyhow::anyhow!("Filter source add failed: {e}")
|
||||
})?;
|
||||
filter_src
|
||||
.add(hw_frame)
|
||||
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
|
||||
|
||||
loop {
|
||||
let mut filtered = ff::frame::Video::empty();
|
||||
@@ -352,9 +358,8 @@ impl EncState {
|
||||
let start_ts = self.starting_timestamp.unwrap();
|
||||
|
||||
// SAFETY: avcodec_send_frame sends a valid NV12 VAAPI surface to the encoder.
|
||||
let ret = unsafe {
|
||||
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr())
|
||||
};
|
||||
let ret =
|
||||
unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) };
|
||||
if ret < 0 {
|
||||
bail!("avcodec_send_frame failed: error {ret}");
|
||||
}
|
||||
@@ -400,9 +405,9 @@ impl EncState {
|
||||
|
||||
// Write trailer only if at least one frame was encoded.
|
||||
if self.frames_written {
|
||||
self.octx.write_trailer().map_err(|e| {
|
||||
anyhow::anyhow!("Failed to write trailer: {e}")
|
||||
})?;
|
||||
self.octx
|
||||
.write_trailer()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -440,9 +445,8 @@ impl EncState {
|
||||
}
|
||||
|
||||
pkt.set_stream(0);
|
||||
pkt.write_interleaved(&mut self.octx).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to write packet: {e}")
|
||||
})?;
|
||||
pkt.write_interleaved(&mut self.octx)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
|
||||
|
||||
self.frames_written = true;
|
||||
}
|
||||
@@ -459,16 +463,19 @@ fn build_filter_graph(
|
||||
frames_rgb: &AvHwFrameCtx,
|
||||
width: u32,
|
||||
height: u32,
|
||||
_enc_width: u32,
|
||||
_enc_height: u32,
|
||||
fps: u32,
|
||||
transform: Transform,
|
||||
) -> Result<ff::filter::Graph> {
|
||||
let mut graph = ff::filter::Graph::new();
|
||||
|
||||
let buffersrc = ff::filter::find("buffer")
|
||||
.ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
|
||||
let buffersrc =
|
||||
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
|
||||
let buffersink = ff::filter::find("buffersink")
|
||||
.ok_or_else(|| anyhow::anyhow!("filter 'buffersink' not found"))?;
|
||||
let format_filter = ff::filter::find("format")
|
||||
.ok_or_else(|| anyhow::anyhow!("filter 'format' not found"))?;
|
||||
let format_filter =
|
||||
ff::filter::find("format").ok_or_else(|| anyhow::anyhow!("filter 'format' not found"))?;
|
||||
let scale_vaapi = ff::filter::find("scale_vaapi")
|
||||
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
|
||||
|
||||
@@ -491,7 +498,10 @@ fn build_filter_graph(
|
||||
(*par).format = Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32;
|
||||
(*par).width = width as i32;
|
||||
(*par).height = height as i32;
|
||||
(*par).time_base = ffi::AVRational { num: 1, den: fps as i32 };
|
||||
(*par).time_base = ffi::AVRational {
|
||||
num: 1,
|
||||
den: fps as i32,
|
||||
};
|
||||
(*par).hw_frames_ctx = frames_rgb.ref_clone();
|
||||
let ret = ffi::av_buffersrc_parameters_set(src_ctx.as_mut_ptr(), par);
|
||||
ffi::av_freep(par as *mut _ as *mut _);
|
||||
@@ -503,7 +513,7 @@ fn build_filter_graph(
|
||||
// format filter: negotiate pixel format to NV12
|
||||
let mut fmt_ctx = graph.add(&format_filter, "fmt", "pix_fmts=nv12")?;
|
||||
|
||||
// scale_vaapi: hardware scaling and colourspace conversion
|
||||
// scale_vaapi: hardware scaling and colourspace conversion (keeps original dimensions)
|
||||
let mut scale_ctx = graph.add(&scale_vaapi, "scale", &format!("{width}:{height}"))?;
|
||||
// SAFETY: scale_vaapi needs hw_device_ctx for VAAPI device access.
|
||||
unsafe {
|
||||
@@ -513,14 +523,43 @@ fn build_filter_graph(
|
||||
// buffersink
|
||||
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
|
||||
|
||||
// Link: src -> format -> scale -> sink
|
||||
// Build filter chain: src -> format -> scale -> [transpose] -> sink
|
||||
src_ctx.link(0, &mut fmt_ctx, 0);
|
||||
fmt_ctx.link(0, &mut scale_ctx, 0);
|
||||
scale_ctx.link(0, &mut sink_ctx, 0);
|
||||
|
||||
graph.validate().map_err(|e| {
|
||||
anyhow::anyhow!("Filter graph validation failed: {e}")
|
||||
})?;
|
||||
match transform {
|
||||
Transform::Normal90 | Transform::Normal270 => {
|
||||
let transpose = ff::filter::find("transpose_vaapi")
|
||||
.ok_or_else(|| anyhow::anyhow!("filter 'transpose_vaapi' not found"))?;
|
||||
let dir_val = match transform {
|
||||
Transform::Normal90 => "1",
|
||||
Transform::Normal270 => "2",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
|
||||
// SAFETY: transpose_vaapi needs hw_device_ctx for VAAPI device access.
|
||||
unsafe {
|
||||
(*trans_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
|
||||
}
|
||||
fmt_ctx.link(0, &mut scale_ctx, 0);
|
||||
scale_ctx.link(0, &mut trans_ctx, 0);
|
||||
trans_ctx.link(0, &mut sink_ctx, 0);
|
||||
}
|
||||
Transform::Normal180 => {
|
||||
tracing::warn!(
|
||||
"Normal180 transform detected; rotation correction deferred to follow-up"
|
||||
);
|
||||
fmt_ctx.link(0, &mut scale_ctx, 0);
|
||||
scale_ctx.link(0, &mut sink_ctx, 0);
|
||||
}
|
||||
_ => {
|
||||
fmt_ctx.link(0, &mut scale_ctx, 0);
|
||||
scale_ctx.link(0, &mut sink_ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
graph
|
||||
.validate()
|
||||
.map_err(|e| anyhow::anyhow!("Filter graph validation failed: {e}"))?;
|
||||
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
16
src/state.rs
16
src/state.rs
@@ -35,7 +35,7 @@ use crate::args::Args;
|
||||
use crate::avhw::{AvHwDevCtx, EncState};
|
||||
use crate::cap_wlr_screencopy::CapWlrScreencopy;
|
||||
use crate::fps_limit::FpsLimit;
|
||||
use crate::transform::Transform;
|
||||
use crate::transform::{transpose_if_transform_transposed, Transform};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CaptureSource trait
|
||||
@@ -429,14 +429,19 @@ impl<S: CaptureSource> State<S> {
|
||||
});
|
||||
let gop_size = self.args.gop_size.unwrap_or(self.args.fps);
|
||||
let fps = self.args.fps;
|
||||
let (enc_w, enc_h) =
|
||||
transpose_if_transform_transposed(output_info.transform, width as i32, height as i32);
|
||||
let enc = match EncState::new(
|
||||
&drm_path,
|
||||
Path::new(&self.args.output),
|
||||
width,
|
||||
height,
|
||||
enc_w as u32,
|
||||
enc_h as u32,
|
||||
bitrate,
|
||||
gop_size,
|
||||
fps,
|
||||
output_info.transform,
|
||||
) {
|
||||
Ok(enc) => enc,
|
||||
Err(e) => {
|
||||
@@ -473,7 +478,7 @@ impl<S: CaptureSource> State<S> {
|
||||
match pos {
|
||||
Some(i) => Some(i),
|
||||
None => {
|
||||
let all_probed = outputs.iter().all(|o| o.done_count >= 2);
|
||||
let all_probed = outputs.iter().all(|o| o.done_count >= 1);
|
||||
if all_probed {
|
||||
let available: Vec<&str> =
|
||||
outputs.iter().filter_map(|o| o.name.as_deref()).collect();
|
||||
@@ -487,7 +492,7 @@ impl<S: CaptureSource> State<S> {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else if outputs.iter().all(|o| o.done_count >= 2) {
|
||||
} else if outputs.iter().all(|o| o.done_count >= 1) {
|
||||
if outputs.is_empty() {
|
||||
return false;
|
||||
}
|
||||
@@ -635,6 +640,7 @@ impl<S: CaptureSource> Dispatch<WlRegistry, GlobalListContents> for State<S> {
|
||||
qhandle: &QueueHandle<State<S>>,
|
||||
) {
|
||||
use wayland_client::protocol::wl_registry::Event as RegistryEvent;
|
||||
tracing::debug!("Dispatch<WlRegistry>::event fired: {:?}", event);
|
||||
|
||||
match event {
|
||||
RegistryEvent::Global {
|
||||
@@ -793,7 +799,7 @@ impl<S: CaptureSource> Dispatch<WlOutput, OutputId> for State<S> {
|
||||
if let EncConstructionStage::ProbingOutputs { outputs, .. } = &mut state.stage {
|
||||
if let Some(info) = outputs.get_mut(idx) {
|
||||
info.done_count += 1;
|
||||
if info.done_count >= 2 {
|
||||
if info.done_count >= 1 {
|
||||
state.try_finalize_output(idx);
|
||||
}
|
||||
}
|
||||
@@ -849,7 +855,7 @@ impl<S: CaptureSource> Dispatch<ZxdgOutputV1, OutputId> for State<S> {
|
||||
if let EncConstructionStage::ProbingOutputs { outputs, .. } = &mut state.stage {
|
||||
if let Some(info) = outputs.get_mut(idx) {
|
||||
info.done_count += 1;
|
||||
if info.done_count >= 2 {
|
||||
if info.done_count >= 1 {
|
||||
state.try_finalize_output(idx);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user