feat: Phase 1 MVP with audit fixes — Wayland screen capture + VAAPI encoding
Phase 1 MVP implementation of wl-webrtc: Wayland screen capture tool with hardware-accelerated VAAPI H.264 encoding and WebTransport output. Includes all 9 runtime bug fixes from code audit (fix-audit-issues plan): CRITICAL: - C2: h264_metadata BSF with repeat_sps/repeat_pps in encode pipeline - C4: FpsLimit wired as timing gate in on_copy_complete HIGH: - C3+A2: DRM device discovery via dmabuf feedback MainDevice event, unified resolve_drm_path() helper (CLI > compositor > auto > fallback) - H2: Separate physical_size (mm) from mode_size (pixels) in wl_output - H1+A3: Multi-output warning + named-output-not-found error MEDIUM: - M5: tv_sec u32->u64 to avoid Y2106 timestamp truncation - M4: Guard against SHM Buffer event (DMA-BUF only) Key components: - src/avhw.rs: FFmpeg VAAPI encoder + filter graph + BSF pipeline - src/state.rs: Wayland event loop + output negotiation + screencopy - src/cap_wlr_screencopy.rs: wlr-screencopy capture source - src/fps_limit.rs: Frame rate limiting with configurable target - src/transform.rs: Frame format conversion utilities
This commit is contained in:
+672
@@ -0,0 +1,672 @@
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use ffmpeg_next as ff;
|
||||
use ffmpeg_next::ffi as ffi;
|
||||
use ffmpeg_next::packet::Mut as _;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BSF FFI — ffmpeg-sys-next does not expose the BSF API; declare manually.
|
||||
// Linked from libavcodec (always present when avcodec feature is enabled).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AVBitStreamFilter {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AVBSFContext {
|
||||
av_class: *const ffi::AVClass,
|
||||
filter: *const AVBitStreamFilter,
|
||||
priv_data: *mut libc::c_void,
|
||||
par_in: *mut ffi::AVCodecParameters,
|
||||
par_out: *mut ffi::AVCodecParameters,
|
||||
time_base_in: ffi::AVRational,
|
||||
time_base_out: ffi::AVRational,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
pub fn av_bsf_get_by_name(name: *const libc::c_char) -> *const AVBitStreamFilter;
|
||||
pub fn av_bsf_alloc(
|
||||
filter: *const AVBitStreamFilter,
|
||||
ctx: *mut *mut AVBSFContext,
|
||||
) -> libc::c_int;
|
||||
pub fn av_bsf_init(ctx: *mut AVBSFContext) -> libc::c_int;
|
||||
pub fn av_bsf_send_packet(ctx: *mut AVBSFContext, pkt: *mut ffi::AVPacket) -> libc::c_int;
|
||||
pub fn av_bsf_receive_packet(ctx: *mut AVBSFContext, pkt: *mut ffi::AVPacket) -> libc::c_int;
|
||||
pub fn av_bsf_free(ctx: *mut *mut AVBSFContext);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AvHwDevCtx
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct AvHwDevCtx {
|
||||
ptr: *mut ffi::AVBufferRef,
|
||||
}
|
||||
|
||||
unsafe impl Send for AvHwDevCtx {}
|
||||
|
||||
impl AvHwDevCtx {
|
||||
pub fn new_vaapi(drm_device: &Path) -> Result<Self> {
|
||||
let device_cstr = CString::new(drm_device.to_str().unwrap())?;
|
||||
let mut p: *mut ffi::AVBufferRef = ptr::null_mut();
|
||||
let ret = unsafe {
|
||||
ffi::av_hwdevice_ctx_create(
|
||||
&mut p,
|
||||
ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI,
|
||||
device_cstr.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
bail!(
|
||||
"Failed to create VAAPI device context from {}: error {ret}",
|
||||
drm_device.display()
|
||||
);
|
||||
}
|
||||
Ok(Self { ptr: p })
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *mut ffi::AVBufferRef {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
pub fn ref_clone(&self) -> *mut ffi::AVBufferRef {
|
||||
// SAFETY: av_buffer_ref atomically increments refcount and returns a new ref.
|
||||
unsafe { ffi::av_buffer_ref(self.ptr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AvHwDevCtx {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
// SAFETY: av_buffer_unref decrements refcount; frees the buffer when it hits zero.
|
||||
unsafe { ffi::av_buffer_unref(&mut self.ptr) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AvHwFrameCtx
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct AvHwFrameCtx {
|
||||
ptr: *mut ffi::AVBufferRef,
|
||||
}
|
||||
|
||||
unsafe impl Send for AvHwFrameCtx {}
|
||||
|
||||
impl AvHwFrameCtx {
|
||||
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");
|
||||
}
|
||||
// SAFETY: p is a valid AVBufferRef from av_hwframe_ctx_alloc.
|
||||
// Its .data field points to an AVHWFramesContext that we must configure.
|
||||
unsafe {
|
||||
let fc = (*p).data as *mut ffi::AVHWFramesContext;
|
||||
(*fc).format = ff::format::Pixel::VAAPI.into();
|
||||
(*fc).sw_format = sw_fmt.into();
|
||||
(*fc).width = w as i32;
|
||||
(*fc).height = h as i32;
|
||||
(*fc).initial_pool_size = 4;
|
||||
}
|
||||
let ret = unsafe { ffi::av_hwframe_ctx_init(p) };
|
||||
if ret < 0 {
|
||||
// SAFETY: p is valid but init failed; clean up.
|
||||
unsafe { ffi::av_buffer_unref(&mut p) };
|
||||
bail!("av_hwframe_ctx_init failed: error {ret}");
|
||||
}
|
||||
Ok(Self { ptr: p })
|
||||
}
|
||||
|
||||
pub fn for_capture(
|
||||
hw_dev: &AvHwDevCtx,
|
||||
w: u32,
|
||||
h: u32,
|
||||
sw_fmt: ff::format::Pixel,
|
||||
) -> Result<Self> {
|
||||
Self::new_inner(hw_dev, w, h, sw_fmt)
|
||||
}
|
||||
|
||||
pub fn for_encode(
|
||||
hw_dev: &AvHwDevCtx,
|
||||
w: u32,
|
||||
h: u32,
|
||||
sw_fmt: ff::format::Pixel,
|
||||
) -> Result<Self> {
|
||||
Self::new_inner(hw_dev, w, h, sw_fmt)
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *mut ffi::AVBufferRef {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
pub fn ref_clone(&self) -> *mut ffi::AVBufferRef {
|
||||
// SAFETY: av_buffer_ref atomically increments refcount and returns a new ref.
|
||||
unsafe { ffi::av_buffer_ref(self.ptr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AvHwFrameCtx {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
// SAFETY: av_buffer_unref decrements refcount; frees when zero.
|
||||
unsafe { ffi::av_buffer_unref(&mut self.ptr) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EncState
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct EncState {
|
||||
enc_video: ff::codec::encoder::video::Video,
|
||||
bsf_ctx: *mut AVBSFContext,
|
||||
frames_rgb: AvHwFrameCtx,
|
||||
frames_yuv: AvHwFrameCtx,
|
||||
video_filter: ff::filter::Graph,
|
||||
hw_device_ctx: AvHwDevCtx,
|
||||
octx: ff::format::context::Output,
|
||||
starting_timestamp: Option<i64>,
|
||||
frames_written: bool,
|
||||
}
|
||||
|
||||
unsafe impl Send for EncState {}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl EncState {
|
||||
pub fn new(
|
||||
drm_device: &Path,
|
||||
output_path: &Path,
|
||||
width: u32,
|
||||
height: u32,
|
||||
bitrate: u64,
|
||||
gop_size: u32,
|
||||
fps: u32,
|
||||
) -> 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,
|
||||
)?;
|
||||
let frames_yuv = AvHwFrameCtx::for_encode(
|
||||
&hw_device_ctx,
|
||||
width,
|
||||
height,
|
||||
ff::format::Pixel::NV12,
|
||||
)?;
|
||||
|
||||
// 3. Find h264_vaapi encoder
|
||||
let codec = ff::encoder::find_by_name("h264_vaapi")
|
||||
.ok_or_else(|| anyhow::anyhow!("h264_vaapi encoder not found"))?;
|
||||
|
||||
let mut enc = {
|
||||
let ctx = ff::codec::Context::new_with_codec(codec);
|
||||
ctx.encoder().video()?
|
||||
};
|
||||
|
||||
enc.set_width(width);
|
||||
enc.set_height(height);
|
||||
enc.set_format(ff::format::Pixel::VAAPI);
|
||||
enc.set_bit_rate(bitrate as usize);
|
||||
enc.set_gop(gop_size);
|
||||
enc.set_time_base(ff::Rational::new(1, fps as i32));
|
||||
enc.set_max_b_frames(0);
|
||||
|
||||
// SAFETY: AV_CODEC_FLAG_GLOBAL_HEADER must be set BEFORE opening the encoder.
|
||||
// It triggers SPS/PPS extradata generation needed by the muxer for
|
||||
// Annex B to AVCC conversion.
|
||||
unsafe {
|
||||
(*enc.as_mut_ptr()).flags |= ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32;
|
||||
}
|
||||
// SAFETY: Assign hw device and frames ctx to the encoder.
|
||||
unsafe {
|
||||
(*enc.as_mut_ptr()).hw_device_ctx = hw_device_ctx.ref_clone();
|
||||
(*enc.as_mut_ptr()).hw_frames_ctx = frames_yuv.ref_clone();
|
||||
}
|
||||
|
||||
// 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 enc_video = opened.0;
|
||||
|
||||
// --- BSF init (after encoder open, before filter graph) ---
|
||||
// SAFETY: av_bsf_get_by_name returns a pointer to a static filter definition.
|
||||
let bsf_name = CString::new("h264_metadata").unwrap();
|
||||
let filter = unsafe { av_bsf_get_by_name(bsf_name.as_ptr()) };
|
||||
if filter.is_null() {
|
||||
bail!("h264_metadata BSF not found in FFmpeg build");
|
||||
}
|
||||
|
||||
let mut bsf_ctx: *mut AVBSFContext = ptr::null_mut();
|
||||
let ret = unsafe { av_bsf_alloc(filter, &mut bsf_ctx) };
|
||||
if ret < 0 {
|
||||
bail!("av_bsf_alloc failed: error {ret}");
|
||||
}
|
||||
|
||||
// SAFETY: avcodec_parameters_from_context copies FROM AVCodecContext TO AVCodecParameters.
|
||||
let ret = unsafe {
|
||||
ffi::avcodec_parameters_from_context((*bsf_ctx).par_in, enc_video.as_ptr())
|
||||
};
|
||||
if ret < 0 {
|
||||
// SAFETY: bsf_ctx was allocated but not yet initialized — safe to free
|
||||
unsafe { av_bsf_free(&mut bsf_ctx) };
|
||||
bail!("avcodec_parameters_from_context for BSF failed: error {ret}");
|
||||
}
|
||||
|
||||
// SAFETY: time_base_in is a plain AVRational field, safe to write
|
||||
unsafe {
|
||||
(*bsf_ctx).time_base_in = (*enc_video.as_ptr()).time_base;
|
||||
}
|
||||
|
||||
// Set repeat_sps=1
|
||||
let key_sps = CString::new("repeat_sps").unwrap();
|
||||
let val_one = CString::new("1").unwrap();
|
||||
let ret = unsafe {
|
||||
ffi::av_opt_set((*bsf_ctx).priv_data, key_sps.as_ptr(), val_one.as_ptr(), 0)
|
||||
};
|
||||
if ret < 0 {
|
||||
// SAFETY: bsf_ctx allocated but not fully initialized — safe to free
|
||||
unsafe { av_bsf_free(&mut bsf_ctx) };
|
||||
bail!("av_opt_set repeat_sps failed: error {ret}");
|
||||
}
|
||||
|
||||
// Set repeat_pps=1
|
||||
let key_pps = CString::new("repeat_pps").unwrap();
|
||||
let ret = unsafe {
|
||||
ffi::av_opt_set((*bsf_ctx).priv_data, key_pps.as_ptr(), val_one.as_ptr(), 0)
|
||||
};
|
||||
if ret < 0 {
|
||||
// SAFETY: bsf_ctx allocated, repeat_sps set but not init'd — safe to free
|
||||
unsafe { av_bsf_free(&mut bsf_ctx) };
|
||||
bail!("av_opt_set repeat_pps failed: error {ret}");
|
||||
}
|
||||
|
||||
// Initialize BSF
|
||||
let ret = unsafe { av_bsf_init(bsf_ctx) };
|
||||
if ret < 0 {
|
||||
// SAFETY: bsf_ctx allocated, params set but init failed — safe to free
|
||||
unsafe { av_bsf_free(&mut bsf_ctx) };
|
||||
bail!("av_bsf_init failed: error {ret}");
|
||||
}
|
||||
|
||||
// 5. Filter graph (inline)
|
||||
let video_filter =
|
||||
build_filter_graph(&hw_device_ctx, &frames_rgb, width, height, fps)?;
|
||||
|
||||
// 6. Muxer setup (strict order)
|
||||
let output_cstr = CString::new(output_path.to_str().unwrap())?;
|
||||
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
|
||||
|
||||
// SAFETY: avformat_alloc_output_context2 creates format context from
|
||||
// the file extension. Does NOT open the file.
|
||||
let ret = unsafe {
|
||||
ffi::avformat_alloc_output_context2(
|
||||
&mut fmt_ctx_ptr,
|
||||
ptr::null_mut(),
|
||||
ptr::null(),
|
||||
output_cstr.as_ptr(),
|
||||
)
|
||||
};
|
||||
if ret < 0 || fmt_ctx_ptr.is_null() {
|
||||
bail!("Failed to allocate output format context: error {ret}");
|
||||
}
|
||||
|
||||
// SAFETY: avformat_query_codec checks codec+format compatibility.
|
||||
let codec_id = unsafe { (*enc_video.as_ptr()).codec_id };
|
||||
let oformat = unsafe { (*fmt_ctx_ptr).oformat };
|
||||
let compat = unsafe {
|
||||
ffi::avformat_query_codec(oformat, codec_id, ffi::FF_COMPLIANCE_NORMAL as i32)
|
||||
};
|
||||
if compat < 0 {
|
||||
bail!("H.264 codec not supported by output container format");
|
||||
}
|
||||
|
||||
// SAFETY: avformat_new_stream creates a new stream in the format context.
|
||||
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
|
||||
if stream_ptr.is_null() {
|
||||
bail!("Failed to create new stream in output context");
|
||||
}
|
||||
|
||||
// SAFETY: avcodec_parameters_from_context copies encoder params + extradata.
|
||||
let ret = unsafe {
|
||||
ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr())
|
||||
};
|
||||
if ret < 0 {
|
||||
bail!("Failed to copy encoder parameters to stream: error {ret}");
|
||||
}
|
||||
|
||||
// SAFETY: Copy encoder time_base to stream.
|
||||
unsafe {
|
||||
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
||||
}
|
||||
|
||||
// SAFETY: avio_open opens the output file for writing.
|
||||
let ret = unsafe {
|
||||
ffi::avio_open(
|
||||
&mut (*fmt_ctx_ptr).pb,
|
||||
output_cstr.as_ptr(),
|
||||
ffi::AVIO_FLAG_WRITE,
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
bail!(
|
||||
"Failed to open output file '{}': error {ret}",
|
||||
output_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// SAFETY: avformat_write_header writes the container header.
|
||||
let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) };
|
||||
if ret < 0 {
|
||||
bail!("Failed to write output header: error {ret}");
|
||||
}
|
||||
|
||||
// SAFETY: We created fmt_ctx_ptr above and it's valid.
|
||||
let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
|
||||
|
||||
Ok(Self {
|
||||
enc_video,
|
||||
bsf_ctx,
|
||||
frames_rgb,
|
||||
frames_yuv,
|
||||
video_filter,
|
||||
hw_device_ctx,
|
||||
octx,
|
||||
starting_timestamp: None,
|
||||
frames_written: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn frames_rgb(&self) -> &AvHwFrameCtx {
|
||||
&self.frames_rgb
|
||||
}
|
||||
|
||||
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
|
||||
let mut filter_src_ctx = self.video_filter.get("in").unwrap();
|
||||
let mut filter_src = filter_src_ctx.source();
|
||||
let mut filter_sink_ctx = self.video_filter.get("out").unwrap();
|
||||
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}")
|
||||
})?;
|
||||
|
||||
loop {
|
||||
let mut filtered = ff::frame::Video::empty();
|
||||
match filter_sink.frame(&mut filtered) {
|
||||
Ok(()) => {
|
||||
if filtered.pts().is_none() {
|
||||
filtered.set_pts(hw_frame.pts());
|
||||
}
|
||||
}
|
||||
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break,
|
||||
Err(e) => bail!("Filter sink get frame failed: {e}"),
|
||||
}
|
||||
|
||||
let pts = filtered.pts().unwrap_or(0);
|
||||
if self.starting_timestamp.is_none() {
|
||||
self.starting_timestamp = Some(pts);
|
||||
}
|
||||
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())
|
||||
};
|
||||
if ret < 0 {
|
||||
bail!("avcodec_send_frame failed: error {ret}");
|
||||
}
|
||||
self.drain_encoder(start_ts)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<()> {
|
||||
// Flush filter graph
|
||||
let mut filter_src_ctx = self.video_filter.get("in").unwrap();
|
||||
let mut filter_src = filter_src_ctx.source();
|
||||
let _ = filter_src.flush();
|
||||
|
||||
// Drain filter
|
||||
let mut filter_sink_ctx = self.video_filter.get("out").unwrap();
|
||||
let mut filter_sink = filter_sink_ctx.sink();
|
||||
loop {
|
||||
let mut filtered = ff::frame::Video::empty();
|
||||
match filter_sink.frame(&mut filtered) {
|
||||
Ok(()) => {
|
||||
let start_ts = self.starting_timestamp.unwrap_or(0);
|
||||
let ret = unsafe {
|
||||
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr())
|
||||
};
|
||||
if ret < 0 {
|
||||
bail!("avcodec_send_frame failed during flush: error {ret}");
|
||||
}
|
||||
self.drain_encoder(start_ts)?;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: Sending null frame signals end of stream.
|
||||
unsafe {
|
||||
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
|
||||
}
|
||||
|
||||
let start_ts = self.starting_timestamp.unwrap_or(0);
|
||||
self.drain_encoder(start_ts)?;
|
||||
|
||||
// SAFETY: Sending null packet signals end-of-stream to BSF
|
||||
unsafe { av_bsf_send_packet(self.bsf_ctx, ptr::null_mut()) };
|
||||
loop {
|
||||
let mut bsf_pkt = ff::Packet::empty();
|
||||
let ret = unsafe {
|
||||
av_bsf_receive_packet(self.bsf_ctx, bsf_pkt.as_mut_ptr())
|
||||
};
|
||||
if ret < 0 { break; }
|
||||
let enc_tb = self.enc_video.time_base();
|
||||
let stream_tb = unsafe {
|
||||
let streams = (*self.octx.as_ptr()).streams;
|
||||
let st = *streams.add(0);
|
||||
ff::Rational::from((*st).time_base)
|
||||
};
|
||||
bsf_pkt.rescale_ts(enc_tb, stream_tb);
|
||||
if let Some(pts) = bsf_pkt.pts() {
|
||||
bsf_pkt.set_pts(Some(pts - start_ts));
|
||||
}
|
||||
if let Some(dts) = bsf_pkt.dts() {
|
||||
bsf_pkt.set_dts(Some(dts - start_ts));
|
||||
}
|
||||
bsf_pkt.set_stream(0);
|
||||
bsf_pkt.write_interleaved(&mut self.octx).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to write BSF flush packet: {e}")
|
||||
})?;
|
||||
self.frames_written = true;
|
||||
}
|
||||
|
||||
// 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}")
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drain_encoder(&mut self, start_ts: i64) -> Result<()> {
|
||||
let stream_index: i32 = 0;
|
||||
loop {
|
||||
let mut pkt = ff::Packet::empty();
|
||||
// SAFETY: avcodec_receive_packet retrieves an encoded packet.
|
||||
let ret = unsafe {
|
||||
ffi::avcodec_receive_packet(self.enc_video.as_mut_ptr(), pkt.as_mut_ptr())
|
||||
};
|
||||
if ret < 0 {
|
||||
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
|
||||
break;
|
||||
}
|
||||
bail!("avcodec_receive_packet failed: error {ret}");
|
||||
}
|
||||
|
||||
// SAFETY: av_bsf_send_packet sends the encoded packet through the BSF filter.
|
||||
// On success, the BSF takes ownership of the packet data (via av_packet_move_ref).
|
||||
let ret = unsafe { av_bsf_send_packet(self.bsf_ctx, pkt.as_mut_ptr()) };
|
||||
if ret == ffi::AVERROR(ffi::EAGAIN) {
|
||||
// BSF buffer full — break and retry next drain cycle
|
||||
break;
|
||||
}
|
||||
if ret < 0 {
|
||||
bail!("av_bsf_send_packet failed: error {ret}");
|
||||
}
|
||||
|
||||
// Drain all BSF output packets
|
||||
loop {
|
||||
let mut bsf_pkt = ff::Packet::empty();
|
||||
// SAFETY: av_bsf_receive_packet retrieves a BSF-processed packet.
|
||||
let ret = unsafe {
|
||||
av_bsf_receive_packet(self.bsf_ctx, bsf_pkt.as_mut_ptr())
|
||||
};
|
||||
if ret == ffi::AVERROR(ffi::EAGAIN) {
|
||||
break; // No more output yet
|
||||
}
|
||||
if ret == ffi::AVERROR_EOF {
|
||||
break; // BSF drained
|
||||
}
|
||||
if ret < 0 {
|
||||
bail!("av_bsf_receive_packet failed: error {ret}");
|
||||
}
|
||||
|
||||
// Rescale and offset on BSF output packet (NOT original pkt)
|
||||
let enc_tb = self.enc_video.time_base();
|
||||
let stream_tb = unsafe {
|
||||
let streams = (*self.octx.as_ptr()).streams;
|
||||
let st = *streams.add(0);
|
||||
ff::Rational::from((*st).time_base)
|
||||
};
|
||||
bsf_pkt.rescale_ts(enc_tb, stream_tb);
|
||||
|
||||
if let Some(pts) = bsf_pkt.pts() {
|
||||
bsf_pkt.set_pts(Some(pts - start_ts));
|
||||
}
|
||||
if let Some(dts) = bsf_pkt.dts() {
|
||||
bsf_pkt.set_dts(Some(dts - start_ts));
|
||||
}
|
||||
|
||||
bsf_pkt.set_stream(stream_index as usize);
|
||||
bsf_pkt.write_interleaved(&mut self.octx).map_err(|e| {
|
||||
anyhow::anyhow!("Failed to write packet: {e}")
|
||||
})?;
|
||||
|
||||
self.frames_written = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EncState {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: av_bsf_free releases the BSF context and all associated resources.
|
||||
// It handles null safely (returns immediately if *pctx is null).
|
||||
if !self.bsf_ctx.is_null() {
|
||||
unsafe { av_bsf_free(&mut self.bsf_ctx) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter graph (inline)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn build_filter_graph(
|
||||
hw_dev: &AvHwDevCtx,
|
||||
frames_rgb: &AvHwFrameCtx,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
) -> 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 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 scale_vaapi = ff::filter::find("scale_vaapi")
|
||||
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
|
||||
|
||||
// buffersrc — use AVBufferSrcParameters to set hw_frames_ctx properly
|
||||
let args = format!(
|
||||
"video_size={}x{}:pix_fmt={}:time_base=1/{fps}:pixel_aspect=1/1",
|
||||
width,
|
||||
height,
|
||||
Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32,
|
||||
);
|
||||
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
|
||||
|
||||
// SAFETY: av_buffersrc_parameters_alloc allocates params for the buffersrc.
|
||||
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
|
||||
if par.is_null() {
|
||||
bail!("av_buffersrc_parameters_alloc returned null");
|
||||
}
|
||||
// SAFETY: Set hw_frames_ctx on the buffersrc parameters, then apply.
|
||||
unsafe {
|
||||
(*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).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 _);
|
||||
if ret < 0 {
|
||||
bail!("av_buffersrc_parameters_set failed: error {ret}");
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
let mut scale_ctx = graph.add(&scale_vaapi, "scale", &format!("{width}:{height}"))?;
|
||||
// SAFETY: scale_vaapi needs hw_device_ctx for VAAPI device access.
|
||||
unsafe {
|
||||
(*scale_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
|
||||
}
|
||||
|
||||
// buffersink
|
||||
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
|
||||
|
||||
// Link: src -> format -> scale -> 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}")
|
||||
})?;
|
||||
|
||||
Ok(graph)
|
||||
}
|
||||
Reference in New Issue
Block a user