refactor(host): hoist the libav poll_encoder drain + PollOutcome into encode/libav.rs
The three libavcodec backends each carried a byte-identical single-packet receive_packet drain. Move it once into the shared Tier-2 glue as poll_encoder -> PollOutcome (the richest form: Packet / Again / Eof), and have the callers narrow it: - Linux NVENC (encode/linux/mod.rs): poll() matches the shared fn, collapsing Again|Eof to Ok(None) — was an inlined match, now one call. - VAAPI (encode/linux/vaapi.rs): drop the local poll_encoder; the blocking budget loop lets Again|Eof fall through to the deadline check, byte-identical to the old Option::None path. - Windows AMF/QSV (encode/windows/ffmpeg_win.rs): drop the local PollOutcome + poll_encoder; its deadline-driven drain already matches PollOutcome, so only the import changes. No behavior change on any backend. Still a plain monomorphic free fn over a borrowed &mut Encoder — no new per-frame dyn/Box/alloc; the only allocation is the same bitstream to_vec() each path already made. Drops the now-unused ffmpeg::Packet import from all three. Linux check + clippy green (nvenc,vulkan-encode,pyrowave) on RTX 5070 Ti; ffmpeg_win.rs is Windows-cfg, pending .173 compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,12 @@
|
||||
//! (`encode/windows/ffmpeg_win.rs`) — so the byte-identical pieces live once (plan §2.2, the Tier-2
|
||||
//! gap). Free functions and consts over borrowed handles; nothing here is per-frame `dyn`,
|
||||
//! allocating, or on the zero-copy ingest path.
|
||||
use crate::encode::EncodedFrame;
|
||||
use anyhow::{Context, Result};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use ffmpeg_next::ffi; // = ffmpeg_sys_next
|
||||
use ffmpeg_next::format::Pixel;
|
||||
use ffmpeg_next::{encoder, Packet};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
/// swscale: nearest-neighbour scaler flag (`SWS_POINT`). We never rescale (src dims == dst dims), so
|
||||
@@ -21,3 +25,42 @@ pub(crate) const SWS_CS_BT2020: c_int = 9;
|
||||
pub(crate) fn pixel_to_av(p: Pixel) -> ffi::AVPixelFormat {
|
||||
ffi::AVPixelFormat::from(p)
|
||||
}
|
||||
|
||||
/// One `receive_packet` attempt, with the not-ready states kept distinct so a blocking drain can
|
||||
/// tell "still encoding" (retry) from "stream over" (stop). The Linux NVENC/VAAPI polls collapse
|
||||
/// `Again`/`Eof` to `None`; the Windows AMF/QSV path keeps them apart for its deadline-driven loop.
|
||||
pub(crate) enum PollOutcome {
|
||||
Packet(EncodedFrame),
|
||||
Again,
|
||||
Eof,
|
||||
}
|
||||
|
||||
/// Drain the encoder for one packet (shared across the NVENC/VAAPI/AMF/QSV libav backends). The
|
||||
/// `EncodedFrame`'s only allocation is the `to_vec()` of the bitstream — the same copy each backend
|
||||
/// already made — so this stays off any per-frame `dyn`/`Box`/channel path.
|
||||
pub(crate) fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result<PollOutcome> {
|
||||
let mut pkt = Packet::empty();
|
||||
match enc.receive_packet(&mut pkt) {
|
||||
Ok(()) => {
|
||||
let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default();
|
||||
let pts = pkt.pts().unwrap_or(0).max(0) as u64;
|
||||
Ok(PollOutcome::Packet(EncodedFrame {
|
||||
data,
|
||||
pts_ns: pts * 1_000_000_000 / fps as u64,
|
||||
keyframe: pkt.is_key(),
|
||||
recovery_anchor: false,
|
||||
chunk_aligned: false,
|
||||
}))
|
||||
}
|
||||
// No packet ready yet (need another input frame).
|
||||
Err(ffmpeg::Error::Other { errno })
|
||||
if errno == ffmpeg::util::error::EAGAIN
|
||||
|| errno == ffmpeg::util::error::EWOULDBLOCK =>
|
||||
{
|
||||
Ok(PollOutcome::Again)
|
||||
}
|
||||
// Fully drained after flush().
|
||||
Err(ffmpeg::Error::Eof) => Ok(PollOutcome::Eof),
|
||||
Err(e) => Err(e).context("receive_packet"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ffmpeg::format::Pixel;
|
||||
use ffmpeg::util::frame::Video as VideoFrame;
|
||||
use ffmpeg::{codec, encoder, Dictionary, Packet, Rational};
|
||||
use ffmpeg::{codec, encoder, Dictionary, Rational};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use std::os::raw::c_int;
|
||||
use std::ptr;
|
||||
|
||||
use super::libav::{pixel_to_av, SWS_CS_ITU709, SWS_POINT};
|
||||
use super::libav::{pixel_to_av, poll_encoder, PollOutcome, SWS_CS_ITU709, SWS_POINT};
|
||||
use ffmpeg::ffi; // = ffmpeg_sys_next
|
||||
|
||||
/// The swscale *source* pixel format for a captured packed RGB/BGR layout (the real byte order, not
|
||||
@@ -624,30 +624,11 @@ impl Encoder for NvencEncoder {
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Result<Option<EncodedFrame>> {
|
||||
let mut pkt = Packet::empty();
|
||||
match self.enc.receive_packet(&mut pkt) {
|
||||
Ok(()) => {
|
||||
let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default();
|
||||
let pts = pkt.pts().unwrap_or(0).max(0) as u64;
|
||||
let pts_ns = pts * 1_000_000_000 / self.fps as u64;
|
||||
Ok(Some(EncodedFrame {
|
||||
data,
|
||||
pts_ns,
|
||||
keyframe: pkt.is_key(),
|
||||
recovery_anchor: false,
|
||||
chunk_aligned: false,
|
||||
}))
|
||||
}
|
||||
// No packet ready yet (need another input frame).
|
||||
Err(ffmpeg::Error::Other { errno })
|
||||
if errno == ffmpeg::util::error::EAGAIN
|
||||
|| errno == ffmpeg::util::error::EWOULDBLOCK =>
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
// Fully drained after flush().
|
||||
Err(ffmpeg::Error::Eof) => Ok(None),
|
||||
Err(e) => Err(e).context("receive_packet"),
|
||||
// Non-blocking single drain: a packet ships, EAGAIN (need another input frame) and EOF
|
||||
// (drained after flush) both mean "nothing this tick".
|
||||
match poll_encoder(&mut self.enc, self.fps)? {
|
||||
PollOutcome::Packet(au) => Ok(Some(au)),
|
||||
PollOutcome::Again | PollOutcome::Eof => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use super::{Codec, EncodedFrame, Encoder};
|
||||
use crate::capture::{CapturedFrame, DmabufFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ffmpeg::format::Pixel;
|
||||
use ffmpeg::{codec, encoder, Dictionary, Packet, Rational};
|
||||
use ffmpeg::{codec, encoder, Dictionary, Rational};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::fd::AsRawFd;
|
||||
@@ -34,7 +34,7 @@ use std::os::raw::c_int;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use super::libav::{pixel_to_av, SWS_CS_ITU709, SWS_POINT};
|
||||
use super::libav::{pixel_to_av, poll_encoder, PollOutcome, SWS_CS_ITU709, SWS_POINT};
|
||||
use ffmpeg::ffi; // = ffmpeg_sys_next
|
||||
|
||||
/// `fourcc(a,b,c,d)` — DRM FourCC packing (`a | b<<8 | c<<16 | d<<24`).
|
||||
@@ -271,32 +271,6 @@ pub fn probe_can_encode_444(_codec: Codec) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Drain the encoder for one packet (shared poll logic).
|
||||
fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result<Option<EncodedFrame>> {
|
||||
let mut pkt = Packet::empty();
|
||||
match enc.receive_packet(&mut pkt) {
|
||||
Ok(()) => {
|
||||
let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default();
|
||||
let pts = pkt.pts().unwrap_or(0).max(0) as u64;
|
||||
Ok(Some(EncodedFrame {
|
||||
data,
|
||||
pts_ns: pts * 1_000_000_000 / fps as u64,
|
||||
keyframe: pkt.is_key(),
|
||||
recovery_anchor: false,
|
||||
chunk_aligned: false,
|
||||
}))
|
||||
}
|
||||
Err(ffmpeg::Error::Other { errno })
|
||||
if errno == ffmpeg::util::error::EAGAIN
|
||||
|| errno == ffmpeg::util::error::EWOULDBLOCK =>
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
Err(ffmpeg::Error::Eof) => Ok(None),
|
||||
Err(e) => Err(e).context("receive_packet"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// CPU upload path (Phase 1): swscale RGB→NV12 → upload into a pooled VA surface → encode.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
@@ -1143,10 +1117,15 @@ impl Encoder for VaapiEncoder {
|
||||
.min(std::time::Duration::from_millis(12));
|
||||
let deadline = std::time::Instant::now() + budget;
|
||||
loop {
|
||||
if let Some(au) = poll_encoder(enc, self.fps)? {
|
||||
match poll_encoder(enc, self.fps)? {
|
||||
PollOutcome::Packet(au) => {
|
||||
self.in_flight = self.in_flight.saturating_sub(1);
|
||||
return Ok(Some(au));
|
||||
}
|
||||
// No AU yet (EAGAIN) or drained (EOF) both fall through to the budget check,
|
||||
// exactly as the previous `Option::None` did.
|
||||
PollOutcome::Again | PollOutcome::Eof => {}
|
||||
}
|
||||
// Nothing ready: only wait when a frame is actually in flight (a drained/EOF'd
|
||||
// encoder must not spin the budget), and give the ASIC ~250 µs between checks.
|
||||
if self.in_flight == 0 || std::time::Instant::now() >= deadline {
|
||||
|
||||
@@ -44,7 +44,7 @@ use super::{ChromaFormat, Codec, EncodedFrame, Encoder};
|
||||
use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use ffmpeg::format::Pixel;
|
||||
use ffmpeg::{codec, encoder, Dictionary, Packet, Rational};
|
||||
use ffmpeg::{codec, encoder, Dictionary, Rational};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use std::os::raw::{c_int, c_uint, c_void};
|
||||
use std::ptr;
|
||||
@@ -60,7 +60,9 @@ use windows::Win32::Graphics::Dxgi::Common::{
|
||||
DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_SAMPLE_DESC,
|
||||
};
|
||||
|
||||
use super::libav::{pixel_to_av, SWS_CS_BT2020, SWS_CS_ITU709, SWS_POINT};
|
||||
use super::libav::{
|
||||
pixel_to_av, poll_encoder, PollOutcome, SWS_CS_BT2020, SWS_CS_ITU709, SWS_POINT,
|
||||
};
|
||||
use ffmpeg::ffi; // = ffmpeg_sys_next
|
||||
|
||||
/// `AVD3D11VADeviceContext` (libavutil/hwcontext_d3d11va.h) — mirrored (the ffmpeg-sys bindings
|
||||
@@ -305,40 +307,6 @@ pub fn probe_can_encode(vendor: WinVendor, codec: Codec) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// One `receive_packet` attempt, with the not-ready states kept distinct so the blocking poll
|
||||
/// below can tell "still encoding" (retry) from "stream over" (stop).
|
||||
enum PollOutcome {
|
||||
Packet(EncodedFrame),
|
||||
Again,
|
||||
Eof,
|
||||
}
|
||||
|
||||
/// Drain the encoder for one packet (shared poll logic, identical to the VAAPI/NVENC paths).
|
||||
fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result<PollOutcome> {
|
||||
let mut pkt = Packet::empty();
|
||||
match enc.receive_packet(&mut pkt) {
|
||||
Ok(()) => {
|
||||
let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default();
|
||||
let pts = pkt.pts().unwrap_or(0).max(0) as u64;
|
||||
Ok(PollOutcome::Packet(EncodedFrame {
|
||||
data,
|
||||
pts_ns: pts * 1_000_000_000 / fps as u64,
|
||||
keyframe: pkt.is_key(),
|
||||
recovery_anchor: false,
|
||||
chunk_aligned: false,
|
||||
}))
|
||||
}
|
||||
Err(ffmpeg::Error::Other { errno })
|
||||
if errno == ffmpeg::util::error::EAGAIN
|
||||
|| errno == ffmpeg::util::error::EWOULDBLOCK =>
|
||||
{
|
||||
Ok(PollOutcome::Again)
|
||||
}
|
||||
Err(ffmpeg::Error::Eof) => Ok(PollOutcome::Eof),
|
||||
Err(e) => Err(e).context("receive_packet"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The immediate context of an `ID3D11Device` (for `CopyResource`/`CopySubresourceRegion`).
|
||||
unsafe fn immediate_context(device: &ID3D11Device) -> ID3D11DeviceContext {
|
||||
// windows-rs 0.62: the inherent method takes no args and returns the context (the OutRef form is
|
||||
|
||||
Reference in New Issue
Block a user