BUG-2 (HIGH): SHM Buffer event caused permanent hang In the ZwlrScreencopyFrameV1 dispatcher, receiving a SHM Buffer event left in_flight_surface stuck at AllocQueued forever, preventing queue_alloc_frame() from requesting new frames. Fix: treat Buffer as a metadata offer (v3 protocol), wait for BufferDone to decide failure, and add AllocQueued state guard to LinuxDmabuf handler. BUG-3 (MEDIUM): Portal backend picked wrong GPU on multi-GPU systems state_portal.rs hardcoded /dev/dri/renderD128 then renderD129, which selects the wrong GPU when PipeWire uses a different device. Fix: extract find_drm_render_nodes() as shared utility; defer DRM device selection to first PipeWire frame; test each candidate with av_hwframe_transfer_data to find the GPU that can actually import the DMA-BUF frame. BUG-4 (LOW): VAAPI device context created twice unnecessarily try_finalize_output() created an AvHwDevCtx stored in EverythingButFmt, but negotiate_format() discarded it (_hw_device_ctx) and EncState::new created a new one. Fix: thread the existing hw_device_ctx through negotiate_format() and create_encoder() to EncState::new() which reuses it when provided.
427 lines
16 KiB
Rust
427 lines
16 KiB
Rust
// 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码
|
||
use std::mem;
|
||
use std::os::fd::AsRawFd;
|
||
use std::path::PathBuf;
|
||
|
||
use anyhow::{bail, Result};
|
||
use ffmpeg_next as ff;
|
||
use ffmpeg_next::ffi;
|
||
|
||
use crate::args::Args;
|
||
use crate::avhw::{self, EncState};
|
||
use crate::cap_portal::{CapPortal, PwDmaBufFrame, PwEvent};
|
||
use crate::fps_limit::FpsLimit;
|
||
use crate::transform::Transform;
|
||
|
||
/// 门户采集的阶段状态
|
||
/// - WaitingForFormat: 等待接收到第一帧 DMA-BUF 以确定视频格式参数
|
||
/// - Streaming: 已完成初始化,正在持续编码流
|
||
enum PortalStage {
|
||
WaitingForFormat,
|
||
Streaming,
|
||
}
|
||
|
||
/// 门户模式的主状态机
|
||
///
|
||
/// 负责管理从 PipeWire 采集屏幕帧、通过 VAAPI 硬件编码的完整生命周期。
|
||
/// 工作流程:等待第一帧 → 创建编码器 → 持续编码帧数据。
|
||
pub struct StatePortal {
|
||
/// 当前采集阶段
|
||
stage: PortalStage,
|
||
/// 硬件编码器状态(第一帧到达后才初始化)
|
||
enc: Option<EncState>,
|
||
/// 帧率限制器
|
||
fps_limit: FpsLimit<()>,
|
||
/// PipeWire 屏幕采集端点
|
||
cap: CapPortal,
|
||
/// 命令行参数
|
||
args: Args,
|
||
/// 是否遇到错误
|
||
errored: bool,
|
||
/// 是否为第一帧(首帧跳过帧率限制)
|
||
first_frame: bool,
|
||
/// DRM 渲染设备路径(如 /dev/dri/renderD128);None 表示首帧自动检测
|
||
drm_device: Option<PathBuf>,
|
||
/// 第一帧的时间戳(纳秒),用于计算相对 PTS
|
||
first_pts_ns: Option<i64>,
|
||
}
|
||
|
||
impl StatePortal {
|
||
/// 创建门户状态实例
|
||
///
|
||
/// 初始化 DRM 设备路径和 PipeWire 采集端点,编码器延迟到第一帧到达时创建。
|
||
pub fn new(args: Args) -> Result<Self> {
|
||
let drm_device = resolve_drm_device(&args)?;
|
||
if let Some(ref drm_device) = drm_device {
|
||
tracing::info!("Using DRM device: {}", drm_device.display());
|
||
} else {
|
||
tracing::info!("DRM device auto-detection enabled");
|
||
}
|
||
|
||
let cap = CapPortal::new(&args)?;
|
||
|
||
Ok(Self {
|
||
stage: PortalStage::WaitingForFormat,
|
||
enc: None,
|
||
fps_limit: FpsLimit::new(args.fps),
|
||
cap,
|
||
args,
|
||
errored: false,
|
||
first_frame: true,
|
||
drm_device,
|
||
first_pts_ns: None,
|
||
})
|
||
}
|
||
|
||
/// 轮询 PipeWire 事件并编码帧
|
||
///
|
||
/// 尝试从采集端点接收一帧事件。返回 `Ok(true)` 表示已处理事件,
|
||
/// `Ok(false)` 表示暂无数据。内部根据当前阶段(等待格式/流式)分发处理。
|
||
pub fn poll_and_encode(&mut self) -> Result<bool> {
|
||
let event = match self.cap.frame_receiver().try_recv() {
|
||
Ok(event) => event,
|
||
Err(_) => return Ok(false),
|
||
};
|
||
|
||
match event {
|
||
PwEvent::Frame(frame) => {
|
||
match self.stage {
|
||
PortalStage::WaitingForFormat => {
|
||
// 第一帧到达:记录格式信息并用该分辨率创建编码器
|
||
tracing::info!(
|
||
"First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}",
|
||
frame.width,
|
||
frame.height,
|
||
frame.format,
|
||
frame.stride,
|
||
frame.modifier
|
||
);
|
||
|
||
let drm_path = self.resolve_drm_device_for_frame(&frame)?;
|
||
let enc = avhw::create_encoder(
|
||
&drm_path,
|
||
self.args.output.as_ref(),
|
||
frame.width,
|
||
frame.height,
|
||
self.args.fps,
|
||
Transform::Normal,
|
||
self.args.bitrate,
|
||
self.args.gop_size,
|
||
None,
|
||
)?;
|
||
|
||
self.enc = Some(enc);
|
||
self.stage = PortalStage::Streaming;
|
||
drop(frame);
|
||
}
|
||
PortalStage::Streaming => {
|
||
// 流式阶段:处理每一帧 DMA-BUF 数据
|
||
self.handle_pw_frame(frame)?;
|
||
}
|
||
}
|
||
}
|
||
PwEvent::StreamEnded => {
|
||
// PipeWire 流结束(如用户停止了屏幕共享)
|
||
tracing::warn!("PipeWire stream ended");
|
||
self.errored = true;
|
||
}
|
||
PwEvent::Error(e) => {
|
||
// PipeWire 返回错误
|
||
tracing::error!("PipeWire error: {e}");
|
||
self.errored = true;
|
||
}
|
||
}
|
||
|
||
Ok(true)
|
||
}
|
||
|
||
fn resolve_drm_device_for_frame(&mut self, frame: &PwDmaBufFrame) -> Result<PathBuf> {
|
||
if let Some(ref drm) = self.drm_device {
|
||
return Ok(drm.clone());
|
||
}
|
||
|
||
let candidates = crate::state::find_drm_render_nodes();
|
||
if candidates.is_empty() {
|
||
bail!("No DRM render device found. Specify --drm-device.");
|
||
}
|
||
|
||
let mut failures = Vec::new();
|
||
for candidate in &candidates {
|
||
match crate::avhw::test_dma_buf_import(candidate, frame) {
|
||
Ok(()) => {
|
||
tracing::info!(
|
||
"Auto-selected DRM device: {} (can import PipeWire DMA-BUF)",
|
||
candidate.display()
|
||
);
|
||
self.drm_device = Some(candidate.clone());
|
||
return Ok(candidate.clone());
|
||
}
|
||
Err(err) => {
|
||
tracing::debug!(
|
||
"DRM device {} cannot import frame: {err:#}",
|
||
candidate.display()
|
||
);
|
||
failures.push(format!("{}: {err:#}", candidate.display()));
|
||
}
|
||
}
|
||
}
|
||
|
||
bail!(
|
||
"No DRM render device can import the PipeWire DMA-BUF frame. \
|
||
Specify --drm-device. Tried: {}",
|
||
failures.join("; ")
|
||
)
|
||
}
|
||
|
||
/// 处理单帧 DMA-BUF 数据
|
||
///
|
||
/// 完整的帧处理流水线:
|
||
/// 1. 帧率限制(首帧跳过)
|
||
/// 2. 构建 DRM 描述符
|
||
/// 3. 分配 DRM_PRIME 源帧
|
||
/// 4. 分配 VAAPI 硬件目标帧
|
||
/// 5. 通过 DMA-BUF 导入将帧数据导入 VAAPI
|
||
/// 6. 计算 PTS 时间戳
|
||
/// 7. 回收 DRM 描述符内存
|
||
/// 8. 编码输出
|
||
fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> {
|
||
// 1. FPS limiting (first frame bypasses)
|
||
// 帧率限制(首帧跳过限制,确保立即编码)
|
||
if self.first_frame {
|
||
self.first_frame = false;
|
||
} else {
|
||
let now = std::time::Instant::now();
|
||
if self.fps_limit.on_new_frame((), now).is_none() {
|
||
return Ok(());
|
||
}
|
||
}
|
||
|
||
// 2. Build DRM descriptor for DMA-BUF import
|
||
// 根据 DMA-BUF 帧信息构建 FFmpeg DRM 描述符
|
||
let desc = build_drm_descriptor(&frame);
|
||
let desc_box = Box::new(desc);
|
||
|
||
// 3. Allocate raw DRM_PRIME source frame using Video wrapper
|
||
// 分配 DRM_PRIME 格式的源帧,将描述符指针挂载到 data[0]
|
||
let mut raw_frame = ff::frame::Video::empty();
|
||
unsafe {
|
||
let raw_ptr = raw_frame.as_mut_ptr();
|
||
(*raw_ptr).data[0] = Box::into_raw(desc_box) as *mut u8;
|
||
(*raw_ptr).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
|
||
(*raw_ptr).width = frame.width as i32;
|
||
(*raw_ptr).height = frame.height as i32;
|
||
}
|
||
|
||
// 4. Get encoder reference
|
||
// 获取编码器引用
|
||
let enc = match self.enc.as_mut() {
|
||
Some(e) => e,
|
||
None => {
|
||
// Recover the Box to prevent memory leak of the descriptor
|
||
// 编码器未初始化时回收描述符以防止内存泄漏
|
||
unsafe {
|
||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||
if !desc_ptr.is_null() {
|
||
let _ = Box::from_raw(desc_ptr);
|
||
}
|
||
}
|
||
bail!("encoder not initialized");
|
||
}
|
||
};
|
||
|
||
// 5. Allocate VAAPI hardware target frame
|
||
// 分配 VAAPI 硬件帧缓冲区
|
||
let mut hw_frame = ff::frame::Video::empty();
|
||
let ret = unsafe {
|
||
ffi::av_hwframe_get_buffer(enc.frames_rgb().as_ptr(), hw_frame.as_mut_ptr(), 0)
|
||
};
|
||
if ret < 0 {
|
||
// Recover the Box to prevent memory leak of the descriptor
|
||
// 分配失败时回收描述符防止内存泄漏
|
||
unsafe {
|
||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||
if !desc_ptr.is_null() {
|
||
let _ = Box::from_raw(desc_ptr);
|
||
}
|
||
}
|
||
bail!("av_hwframe_get_buffer failed: error {ret}");
|
||
}
|
||
|
||
// 6. Import DMA-BUF into VAAPI via transfer_data
|
||
// 通过 DMA-BUF 导入将帧数据从 DRM 传输到 VAAPI 硬件表面
|
||
let ret = unsafe {
|
||
ffi::av_hwframe_transfer_data(hw_frame.as_mut_ptr(), raw_frame.as_ptr(), 0)
|
||
};
|
||
if ret < 0 {
|
||
// 传输失败时回收描述符防止内存泄漏
|
||
unsafe {
|
||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||
if !desc_ptr.is_null() {
|
||
let _ = Box::from_raw(desc_ptr);
|
||
}
|
||
}
|
||
if ret == -(ffi::EINVAL as i32) {
|
||
bail!(
|
||
"VAAPI does not support DMA-BUF modifier 0x{:X}",
|
||
frame.modifier
|
||
);
|
||
}
|
||
bail!("av_hwframe_transfer_data failed: error {ret}");
|
||
}
|
||
|
||
// 7. Set PTS — convert PipeWire nanoseconds to encoder frame-number units
|
||
// PipeWire PTS is CLOCK_MONOTONIC in nanoseconds.
|
||
// Encoder time_base = 1/fps, so PTS must be in frame numbers.
|
||
// Use elapsed time since first frame to avoid i64 overflow on absolute timestamps.
|
||
//
|
||
// PTS 计算:将 PipeWire 的纳秒时间戳转换为编码器的帧号单位
|
||
// PipeWire 使用 CLOCK_MONOTONIC 纳秒时间戳,编码器 time_base = 1/fps
|
||
// 使用相对时间避免绝对时间戳导致的 i64 溢出
|
||
let fps_i64 = self.args.fps as i64;
|
||
let base_ns = *self.first_pts_ns.get_or_insert(frame.pts.max(0));
|
||
let elapsed_ns = (frame.pts.max(0) - base_ns).max(0);
|
||
let pts = elapsed_ns * fps_i64 / 1_000_000_000;
|
||
unsafe {
|
||
(*hw_frame.as_mut_ptr()).pts = pts;
|
||
}
|
||
|
||
// 8. Recover the Boxed descriptor from raw_frame *before* encoding.
|
||
// av_hwframe_transfer_data has already imported the DMA-BUF into the
|
||
// VAAPI surface, so FFmpeg no longer references the descriptor struct.
|
||
// Doing this before encode_frame ensures the descriptor is reclaimed
|
||
// even if encode_frame returns early via `?`.
|
||
//
|
||
// 在编码前回收描述符内存。
|
||
// 此时 DMA-BUF 数据已导入 VAAPI 表面,FFmpeg 不再引用描述符结构体。
|
||
// 在 encode_frame 之前回收确保即使编码返回错误也能正确释放内存。
|
||
unsafe {
|
||
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
|
||
if !desc_ptr.is_null() {
|
||
let _ = Box::from_raw(desc_ptr);
|
||
}
|
||
}
|
||
|
||
// 9. Encode — safe to early-return via `?` now that descriptor is recovered.
|
||
// 编码帧数据(此时描述符已回收,可安全通过 `?` 提前返回)
|
||
enc.encode_frame(&hw_frame)?;
|
||
|
||
// raw_frame and hw_frame drop here via Video::drop → av_frame_free
|
||
// raw_frame 和 hw_frame 在此处通过 Video::drop → av_frame_free 释放
|
||
Ok(())
|
||
}
|
||
|
||
/// 刷新编码器缓冲区,输出所有剩余帧
|
||
pub fn flush(&mut self) -> Result<()> {
|
||
if let Some(enc) = &mut self.enc {
|
||
enc.flush()?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 关闭状态:刷新编码器并清理资源
|
||
pub fn shutdown(&mut self) {
|
||
if let Err(e) = self.flush() {
|
||
tracing::error!("Flush error during shutdown: {e}");
|
||
}
|
||
tracing::info!("StatePortal shutdown complete");
|
||
}
|
||
|
||
/// 返回是否遇到不可恢复的错误
|
||
pub fn is_errored(&self) -> bool {
|
||
self.errored
|
||
}
|
||
}
|
||
|
||
/// 根据 DMA-BUF 帧信息构建 FFmpeg DRM 帧描述符
|
||
///
|
||
/// 将 PipeWire 提供的 DMA-BUF 参数(fd、偏移量、步长、修饰符等)
|
||
/// 转换为 FFmpeg 的 AVDRMFrameDescriptor 结构体,用于零拷贝硬件导入。
|
||
fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffi::AVDRMFrameDescriptor {
|
||
let mut desc: ffi::AVDRMFrameDescriptor = unsafe { mem::zeroed() };
|
||
|
||
// DMA-BUF 对象层:一个 fd 对应一个内存对象
|
||
desc.nb_objects = 1;
|
||
desc.objects[0].fd = frame.fd.as_raw_fd();
|
||
desc.objects[0].size = 0; // 大小为 0 表示整个 fd
|
||
desc.objects[0].format_modifier = frame.modifier;
|
||
|
||
// 像素格式层:单层单平面布局(如 XR24 格式)
|
||
desc.nb_layers = 1;
|
||
desc.layers[0].format = frame.format;
|
||
desc.layers[0].nb_planes = 1;
|
||
desc.layers[0].planes[0].object_index = 0;
|
||
desc.layers[0].planes[0].offset = frame.offset as isize;
|
||
desc.layers[0].planes[0].pitch = frame.stride as isize;
|
||
|
||
desc
|
||
}
|
||
|
||
/// 解析 DRM 渲染设备路径
|
||
///
|
||
/// 仅使用命令行指定的设备路径;未指定则在首帧到达时自动检测。
|
||
fn resolve_drm_device(args: &Args) -> Result<Option<PathBuf>> {
|
||
if let Some(ref drm) = args.drm_device {
|
||
return Ok(Some(PathBuf::from(drm)));
|
||
}
|
||
Ok(None)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use std::os::fd::{FromRawFd, OwnedFd};
|
||
|
||
/// 创建测试用的 DMA-BUF 帧数据(使用 stderr fd 的副本作为占位)
|
||
fn make_test_frame() -> PwDmaBufFrame {
|
||
// Create a dummy fd from stderr (always valid fd 2)
|
||
let fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) };
|
||
PwDmaBufFrame {
|
||
fd,
|
||
offset: 0,
|
||
stride: 1920 * 4,
|
||
modifier: 0, // DRM_FORMAT_MOD_LINEAR
|
||
width: 1920,
|
||
height: 1080,
|
||
format: 0x34325258, // XR24 little-endian
|
||
pts: 12345,
|
||
}
|
||
}
|
||
|
||
/// 测试 DRM 描述符构建(单平面情况)
|
||
#[test]
|
||
fn build_drm_descriptor_single_plane() {
|
||
let frame = make_test_frame();
|
||
let desc = build_drm_descriptor(&frame);
|
||
|
||
assert_eq!(desc.nb_objects, 1);
|
||
assert_eq!(desc.objects[0].format_modifier, 0);
|
||
assert_eq!(desc.nb_layers, 1);
|
||
assert_eq!(desc.layers[0].format, 0x34325258);
|
||
assert_eq!(desc.layers[0].nb_planes, 1);
|
||
assert_eq!(desc.layers[0].planes[0].object_index, 0);
|
||
assert_eq!(desc.layers[0].planes[0].offset, 0);
|
||
assert_eq!(desc.layers[0].planes[0].pitch, 1920 * 4);
|
||
}
|
||
|
||
/// 测试显式指定 DRM 设备时的解析
|
||
#[test]
|
||
fn resolve_drm_device_explicit() {
|
||
let args = Args {
|
||
output: "test.mp4".to_string(),
|
||
output_name: None,
|
||
fps: 30,
|
||
codec: "h264".to_string(),
|
||
hw_accel: "vaapi".to_string(),
|
||
drm_device: Some("/dev/dri/renderD128".to_string()),
|
||
bitrate: None,
|
||
gop_size: None,
|
||
verbose: false,
|
||
backend: None,
|
||
port: 0,
|
||
};
|
||
let result = resolve_drm_device(&args);
|
||
assert!(result.is_ok());
|
||
assert_eq!(result.unwrap(), std::path::PathBuf::from("/dev/dri/renderD128"));
|
||
}
|
||
}
|