refactor(bin): dedupe av_err_to_string + receive_first_frame + drain_encoder via shared src/bin/common/mod.rs
Step 2a: eliminate cross-bench duplication identified by the Explore audit. Changes: - src/avhw/util.rs: av_err_to_string promoted pub(crate) -> pub (the only change to src/avhw/ in this whole refactor plan). - src/avhw/mod.rs: re-export av_err_to_string; #[allow(unused_imports)] silences rustc's per-bin unused-import false positive (the pub use is consumed by the bench bins, not by the main bin). - src/bin/common/mod.rs (new): shared receive_first_frame + drain_encoder. These were byte-identical between the two bench binaries modulo a type-path alias (ff::codec::encoder::video::Video vs ff::encoder::video::Video) and SAFETY-comment line wrapping. Both binaries now wire it via #[path = "common/mod.rs"] mod common;. - src/bin/vaapi_import_bench.rs: 1039 -> 947 LOC (av_err_to_string, receive_first_frame, drain_encoder all removed; 3 call sites updated). - src/bin/sw_encode_bench.rs: 614 -> 545 LOC (receive_first_frame, drain_encoder removed; 3 call sites updated). - use ffmpeg_next::packet::Mut moved to common/mod.rs (was needed only for pkt.as_mut_ptr() inside drain_encoder). Verification (all green): - cargo build --bins / cargo build --release - cargo test (79 lib + 3 integration = 82 pass, 1 ignored — unchanged) - cargo clippy --all-targets -- -D warnings - cargo fmt --check - Test counts unchanged from baseline
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use ffmpeg_next as ff;
|
||||
use ffmpeg_next::ffi;
|
||||
use ffmpeg_next::packet::Mut;
|
||||
use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent};
|
||||
|
||||
pub fn receive_first_frame(cap: &CapPortal) -> Result<wl_webrtc::cap_portal::PwDmaBufFrame> {
|
||||
// Drain-and-wait loop that mirrors production's repeated-poll semantics
|
||||
// (state_portal.rs::poll_and_encode driven by main.rs's outer loop), but with
|
||||
// a single bounded 10s total deadline appropriate for a bench tool. Unlike a
|
||||
// single 10s blocking wait, this loop actually iterates: each turn drains ALL
|
||||
// pending control events (the ctrl channel is bounded to 8 — a single
|
||||
// if-let would silently miss backlog) and then waits a short slice for a
|
||||
// frame, so StreamEnded/Error arriving mid-wait are observed within ~200ms.
|
||||
const TOTAL_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
const WAIT_SLICE: std::time::Duration = std::time::Duration::from_millis(200);
|
||||
let deadline = Instant::now() + TOTAL_DEADLINE;
|
||||
loop {
|
||||
while let Ok(ctrl) = cap.event_receiver().try_recv() {
|
||||
match ctrl {
|
||||
PwCtrlEvent::StreamEnded => bail!("PipeWire stream ended before first frame"),
|
||||
PwCtrlEvent::FormatChanged { .. } => {}
|
||||
PwCtrlEvent::Error(e) => bail!("PipeWire error: {e}"),
|
||||
}
|
||||
}
|
||||
let remaining = match deadline.checked_duration_since(Instant::now()) {
|
||||
Some(r) if !r.is_zero() => r,
|
||||
_ => bail!("Timeout waiting for first frame (10s)"),
|
||||
};
|
||||
let slice = remaining.min(WAIT_SLICE);
|
||||
match cap.frame_receiver().recv_timeout(slice) {
|
||||
Ok(frame) => return Ok(frame),
|
||||
Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
|
||||
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
|
||||
bail!("PipeWire frame channel disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain_encoder(
|
||||
enc_video: &mut ff::encoder::video::Video,
|
||||
octx: &mut ff::format::context::Output,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let mut pkt = ff::Packet::empty();
|
||||
// SAFETY: enc_video is the opened encoder; pkt is an empty Packet whose
|
||||
// inner AVPacket pointer is valid. avcodec_receive_packet fills pkt with
|
||||
// the next encoded packet, or returns EAGAIN/EOF when drained.
|
||||
let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) };
|
||||
if ret < 0 {
|
||||
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
|
||||
break;
|
||||
}
|
||||
eprintln!("avcodec_receive_packet failed: {ret}");
|
||||
break;
|
||||
}
|
||||
|
||||
let enc_tb = enc_video.time_base();
|
||||
// SAFETY: octx.as_ptr() is a valid AVFormatContext; streams is a NULL-terminated
|
||||
// array of AVStream*. We index [0] which exists because we created exactly one
|
||||
// stream in setup. Reading time_base is a plain AVRational field access.
|
||||
let stream_tb = unsafe {
|
||||
let streams = (*octx.as_ptr()).streams;
|
||||
let st = *streams.add(0);
|
||||
ff::Rational::from((*st).time_base)
|
||||
};
|
||||
pkt.rescale_ts(enc_tb, stream_tb);
|
||||
pkt.set_stream(0);
|
||||
pkt.write_interleaved(octx)
|
||||
.map_err(|e| anyhow::anyhow!("write packet failed: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user