背景 协议 v2 与 vendor nvenc 补丁之后,KeyFrameReq 已成为**真正生效**的恢复主路径 (此前 NVENC 缺 FORCEIDR 标志,请求被静默忽略)。因此周期 IDR 的角色变了: 从「唯一恢复手段」降级为「兜底手段」,只用于防「请求连续丢失」这种极端情况。 既然是兜底,就没必要配得那么长。 问题:三处周期不一致,且都过长 - agent/src/capture.rs NVENC:gop_len = 240 帧 @60fps = 4 秒 - agent/src/capture.rs 软编: intra_period = 300 帧 @60fps = 5 秒 - agent-core/src/lib.rs Linux: intra_period = 300 帧 @30fps = **10 秒** 同一产品在三种编码后端上的恢复等待时间差 2.5 倍,且 Linux 端最差。 改动 - 新增 agent/src/capture.rs::INTRA_PERIOD_FRAMES = 60(Windows 两个编码器共用) - agent-core 的 EncoderSettings::default() 改为按 max_fps 推导(1 秒对应帧数), 避免以后调帧率时周期又漂移 代价 IDR 是整帧内编码,实测一次约 95-120KB,而 P 帧约 3-10KB。1 秒一次 IDR 约 增加 0.8Mbps 开销,在 5Mbps 码率下可接受。 验证(dump_stream 观察 150 帧) [ 0] seq=0 IDR ← 首帧 [ 60] seq=60 IDR ← 间隔精确 60 帧 [120] seq=120 IDR @60fps 即每 1 秒一个自动 IDR,与 gop_len=60 配置一致。
580 lines
28 KiB
Rust
580 lines
28 KiB
Rust
//! DXGI Desktop Duplication 采集(Windows 8+)。
|
||
//! 编码器自动选择:NVENC 硬编(N 卡:GPU 内 BGRA→H.264 零转换零拷贝直通)
|
||
//! → openh264 软编兜底(无 N 卡/驱动异常时自动启用)。
|
||
//! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。
|
||
|
||
use anyhow::{anyhow, Result};
|
||
use common::{encode_video_frame, h264_contains_idr, VideoFrameHeader};
|
||
use nvenc::bitstream::BitStream;
|
||
use nvenc::encoder::RegisteredResource;
|
||
use nvenc::session::{InitParams, Session};
|
||
use nvenc::sys::enums::{
|
||
NVencBufferFormat, NVencPicStruct, NVencPicType, NVencParamsRcMode, NVencTuningInfo,
|
||
};
|
||
use nvenc::sys::guids::{NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_P3_GUID};
|
||
use openh264::encoder::{
|
||
BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType,
|
||
};
|
||
use openh264::formats::{BgraSliceU8, YUVBuffer};
|
||
use openh264::OpenH264API;
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
use std::sync::Arc;
|
||
use tokio::sync::watch::Sender;
|
||
use windows::core::Interface;
|
||
use windows::Win32::Foundation::HMODULE;
|
||
use windows::Win32::Graphics::Direct3D::{
|
||
D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL_11_0,
|
||
};
|
||
use windows::Win32::Graphics::Direct3D11::{
|
||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D,
|
||
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_FLAG,
|
||
D3D11_CPU_ACCESS_READ, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE,
|
||
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING,
|
||
};
|
||
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
|
||
use windows::Win32::Graphics::Dxgi::{
|
||
IDXGIDevice, IDXGIOutput1, IDXGIOutputDuplication, IDXGIResource, DXGI_ERROR_WAIT_TIMEOUT,
|
||
DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
|
||
};
|
||
|
||
use std::time::{Duration, Instant};
|
||
|
||
/// 周期 IDR 的帧间隔(兜底恢复点)。
|
||
///
|
||
/// 两个编码器共用同一常量,避免 NVENC 与软编的恢复窗口不一致——此前分别是
|
||
/// 240 帧@60fps=4s 与 300 帧@60fps=5s,而 Linux 端又是 300 帧@30fps=10s。
|
||
///
|
||
/// 取值理由:**主恢复路径是 KeyFrameReq,不是自动 IDR**(协议 v2 后已确认请求
|
||
/// 真正生效,见 vendor/nvenc 的 FORCEIDR 补丁)。自动 IDR 只用于兜底
|
||
/// 「请求连续丢失」这种极端情况。60 帧 @60fps = 1 秒,既足够兜底,
|
||
/// 又不会因过密的整帧内编码抬高码率。
|
||
pub const INTRA_PERIOD_FRAMES: u32 = 60;
|
||
|
||
pub struct Duplication {
|
||
pub device: ID3D11Device,
|
||
context: ID3D11DeviceContext,
|
||
staging_res: ID3D11Resource,
|
||
/// GPU 内中间纹理(NVENC 注册目标;DEFAULT usage,BGRA 与采集一致)
|
||
gpu_res: ID3D11Resource,
|
||
gpu_tex: ID3D11Texture2D,
|
||
dup: IDXGIOutputDuplication,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
}
|
||
|
||
impl Duplication {
|
||
pub fn new() -> Result<Self> {
|
||
unsafe {
|
||
let mut device: Option<ID3D11Device> = None;
|
||
let mut context: Option<ID3D11DeviceContext> = None;
|
||
D3D11CreateDevice(
|
||
None,
|
||
D3D_DRIVER_TYPE_HARDWARE,
|
||
HMODULE(std::ptr::null_mut()),
|
||
D3D11_CREATE_DEVICE_FLAG(0),
|
||
Some(&[D3D_FEATURE_LEVEL_11_0]),
|
||
D3D11_SDK_VERSION,
|
||
Some(&raw mut device),
|
||
None,
|
||
Some(&raw mut context),
|
||
)
|
||
.map_err(|e| anyhow!("D3D11CreateDevice failed: {e}"))?;
|
||
let device = device.ok_or_else(|| anyhow!("no d3d device"))?;
|
||
let context = context.ok_or_else(|| anyhow!("no d3d context"))?;
|
||
|
||
let dxgi: IDXGIDevice = device.cast()?;
|
||
let adapter = dxgi.GetAdapter()?;
|
||
let output = adapter
|
||
.EnumOutputs(0)
|
||
.map_err(|e| anyhow!("no display output: {e}"))?;
|
||
let output1: IDXGIOutput1 = output.cast()?;
|
||
let dup: IDXGIOutputDuplication = output1
|
||
.DuplicateOutput(&device)
|
||
.map_err(|e| anyhow!("DuplicateOutput failed: {e}"))?;
|
||
|
||
let desc: DXGI_OUTDUPL_DESC = dup.GetDesc();
|
||
let (width, height) = (desc.ModeDesc.Width, desc.ModeDesc.Height);
|
||
anyhow::ensure!(width > 0 && height > 0, "bad desktop size");
|
||
|
||
// staging:软编路径 Map 读内存
|
||
let sdesc = D3D11_TEXTURE2D_DESC {
|
||
Width: width,
|
||
Height: height,
|
||
MipLevels: 1,
|
||
ArraySize: 1,
|
||
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
|
||
SampleDesc: DXGI_SAMPLE_DESC {
|
||
Count: 1,
|
||
Quality: 0,
|
||
},
|
||
Usage: D3D11_USAGE_STAGING,
|
||
BindFlags: 0,
|
||
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
||
MiscFlags: 0,
|
||
};
|
||
let mut staging_opt: Option<ID3D11Texture2D> = None;
|
||
device
|
||
.CreateTexture2D(&sdesc, None, Some(&mut staging_opt))
|
||
.map_err(|e| anyhow!("CreateTexture2D failed: {e}"))?;
|
||
let staging = staging_opt.ok_or_else(|| anyhow!("no staging texture"))?;
|
||
let staging_res: ID3D11Resource = staging.cast()?;
|
||
|
||
// GPU 中间纹理:NVENC 路径的注册目标(DEFAULT usage,GPU 可读)
|
||
let gdesc = D3D11_TEXTURE2D_DESC {
|
||
Usage: D3D11_USAGE_DEFAULT,
|
||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||
CPUAccessFlags: 0,
|
||
..sdesc
|
||
};
|
||
let mut gpu_opt: Option<ID3D11Texture2D> = None;
|
||
device
|
||
.CreateTexture2D(&gdesc, None, Some(&mut gpu_opt))
|
||
.map_err(|e| anyhow!("CreateTexture2D(gpu) failed: {e}"))?;
|
||
let gpu_tex = gpu_opt.ok_or_else(|| anyhow!("no gpu texture"))?;
|
||
let gpu_res: ID3D11Resource = gpu_tex.cast()?;
|
||
|
||
Ok(Self {
|
||
device,
|
||
context,
|
||
staging_res,
|
||
gpu_res,
|
||
gpu_tex,
|
||
dup,
|
||
width,
|
||
height,
|
||
})
|
||
}
|
||
}
|
||
|
||
/// true=拿到新帧并已复制;false=超时无更新。
|
||
/// 同时复制到 staging(软编读内存)与 GPU 中间纹理(NVENC 注册输入)。
|
||
pub fn acquire(&mut self, timeout_ms: u32) -> Result<bool> {
|
||
unsafe {
|
||
let mut info = DXGI_OUTDUPL_FRAME_INFO::default();
|
||
let mut resource: Option<IDXGIResource> = None;
|
||
match self.dup.AcquireNextFrame(timeout_ms, &mut info, &mut resource) {
|
||
Ok(()) => {
|
||
let res = resource.ok_or_else(|| anyhow!("no desktop resource"))?;
|
||
let src: ID3D11Resource = res.cast()?;
|
||
self.context.CopyResource(&self.staging_res, &src);
|
||
self.context.CopyResource(&self.gpu_res, &src);
|
||
let _ = self.dup.ReleaseFrame();
|
||
Ok(true)
|
||
}
|
||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => Ok(false),
|
||
Err(e) => Err(e.into()),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 在 staging 映射窗口内执行回调(软编路径)。回调参数 = (像素数据, 行距)。
|
||
pub fn with_mapped<R>(&mut self, f: impl FnOnce(&[u8], usize) -> R) -> Result<R> {
|
||
unsafe {
|
||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||
self.context
|
||
.Map(&self.staging_res, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
|
||
.map_err(|e| anyhow!("Map staging failed: {e}"))?;
|
||
let (w, h) = (self.width as usize, self.height as usize);
|
||
let pitch = mapped.RowPitch as usize;
|
||
let src = std::slice::from_raw_parts(mapped.pData as *const u8, pitch * h);
|
||
let r = f(src, pitch);
|
||
self.context.Unmap(&self.staging_res, 0);
|
||
Ok(r)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// NVENC 硬编封装:H.264 High、CBR 码率控制、UltraLowLatency 调优、无 B 帧。
|
||
/// 输入 = 注册的 GPU 纹理(BGRA 与 DXGI 采集字节序一致,零转换)。
|
||
struct NvencEncoder {
|
||
encoder: nvenc::encoder::Encoder,
|
||
registered: RegisteredResource,
|
||
bitstream: BitStream,
|
||
frame_count: usize,
|
||
}
|
||
|
||
impl NvencEncoder {
|
||
fn new(dup: &mut Duplication, bitrate_bps: u32) -> Result<Self> {
|
||
// 打开会话(无 N 卡 / 驱动不含编码库时在此失败)
|
||
let session: Session<_> = Session::open_dx(&dup.device)
|
||
.map_err(|e| anyhow!("open_dx: {e:?}"))?;
|
||
let codecs = session
|
||
.get_encode_codecs()
|
||
.map_err(|e| anyhow!("codecs: {e:?}"))?;
|
||
anyhow::ensure!(codecs.contains(&NV_ENC_CODEC_H264_GUID), "NVENC no H264 codec");
|
||
let (session, mut config) = session
|
||
.get_encode_preset_config_ex(
|
||
NV_ENC_CODEC_H264_GUID,
|
||
NV_ENC_PRESET_P3_GUID,
|
||
NVencTuningInfo::UltraLowLatency,
|
||
)
|
||
.map_err(|e| anyhow!("preset_config: {e:?}"))?;
|
||
// CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps)
|
||
config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR;
|
||
config.preset_cfg.rc_params.average_bit_rate = bitrate_bps;
|
||
// 周期 IDR 是**兜底恢复点**,不是主恢复路径——主路径是 KeyFrameReq
|
||
// (协议 v2 后已确认真正生效,见 vendor/nvenc 的 FORCEIDR 补丁)。
|
||
//
|
||
// 取值权衡:GOP 越短,丢帧后自动恢复越快,但每次 IDR 都是整帧内编码,
|
||
// 码率开销显著(1080p 桌面 IDR 通常数倍于 P 帧)。
|
||
// 60 帧 @60fps = 1s:既覆盖「KeyFrameReq 连续丢失」这种极端情况,
|
||
// 又把兜底窗口压到 1 秒以内。原先 240 帧 = 4s,在请求已可靠的前提下
|
||
// 只会让受害时长无谓地长 4 倍。
|
||
config.preset_cfg.gop_len = INTRA_PERIOD_FRAMES;
|
||
config.preset_cfg.frame_interval_p = 1; // 无 B 帧
|
||
|
||
let init = InitParams {
|
||
encode_guid: NV_ENC_CODEC_H264_GUID,
|
||
preset_guid: NV_ENC_PRESET_P3_GUID,
|
||
resolution: [dup.width, dup.height],
|
||
aspect_ratio: [dup.width, dup.height],
|
||
frame_rate: [60, 1],
|
||
tuning_info: NVencTuningInfo::UltraLowLatency,
|
||
buffer_format: NVencBufferFormat::ARGB,
|
||
encode_config: &mut config.preset_cfg,
|
||
enable_ptd: true,
|
||
max_encoder_resolution: [dup.width, dup.height],
|
||
};
|
||
let encoder = session
|
||
.init_encoder(init)
|
||
.map_err(|e| anyhow!("init_encoder: {e:?}"))?;
|
||
let registered = encoder
|
||
.register_resource_dx11(&dup.gpu_tex, NVencBufferFormat::ARGB, 0)
|
||
.map_err(|e| anyhow!("register_resource: {e:?}"))?;
|
||
let bitstream = encoder
|
||
.create_bitstream_buffer()
|
||
.map_err(|e| anyhow!("bitstream: {e:?}"))?;
|
||
Ok(Self {
|
||
encoder,
|
||
registered,
|
||
bitstream,
|
||
frame_count: 0,
|
||
})
|
||
}
|
||
|
||
/// 编码当前 GPU 纹理内容,返回 Annex B H.264 码流。
|
||
fn encode(&mut self, force_idr: bool, ts_ms: u64) -> Result<Vec<u8>> {
|
||
// 首帧强制 IDR:解码端从任意时刻接入,没有 IDR 无法解出后续 P 帧
|
||
let ty = if force_idr || self.frame_count == 0 {
|
||
NVencPicType::IDR
|
||
} else {
|
||
NVencPicType::P
|
||
};
|
||
self.encoder.encode_picture(
|
||
&self.registered,
|
||
&self.bitstream,
|
||
self.frame_count,
|
||
ts_ms,
|
||
NVencBufferFormat::ARGB,
|
||
NVencPicStruct::Frame,
|
||
ty,
|
||
None,
|
||
)
|
||
.map_err(|e| anyhow!("encode_picture: {e:?}"))?;
|
||
self.frame_count += 1;
|
||
// 同步读出码流(NVENC 单帧编码 1-3ms,阻塞等待可接受)
|
||
let lock = self
|
||
.bitstream
|
||
.try_lock(true)
|
||
.map_err(|e| anyhow!("bitstream lock: {e:?}"))?;
|
||
let out = lock.as_slice().to_vec();
|
||
drop(lock);
|
||
if out.is_empty() {
|
||
anyhow::bail!("nvenc empty bitstream");
|
||
}
|
||
Ok(out)
|
||
}
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
pub fn desktop_size() -> Result<(u32, u32)> {
|
||
let d = Duplication::new()?;
|
||
Ok((d.width, d.height))
|
||
}
|
||
|
||
/// 采集主循环:独立线程运行;连接断开(通道关闭)时退出。
|
||
/// 通道为 watch 单槽(latest-wins):发送即覆盖,网络慢时不产生积压。
|
||
/// congestion:发送侧 write 超时置位 → 跳过当前帧(不编码不发送),注入率自适应降速。
|
||
pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc<AtomicBool>) {
|
||
let start = Instant::now();
|
||
loop {
|
||
let mut dup = match Duplication::new() {
|
||
Ok(d) => d,
|
||
Err(e) => {
|
||
eprintln!("[capture] init failed: {e}, retry in 2s");
|
||
std::thread::sleep(Duration::from_secs(2));
|
||
continue;
|
||
}
|
||
};
|
||
eprintln!(
|
||
"[capture] duplication started {}x{}",
|
||
dup.width, dup.height
|
||
);
|
||
|
||
// ---- 编码器自动选择:NVENC 硬编优先,失败落回 openh264 软编 ----
|
||
enum EncoderKind {
|
||
Nvenc(Box<NvencEncoder>),
|
||
Soft(Box<openh264::encoder::Encoder>),
|
||
}
|
||
let mut encoder: EncoderKind = match NvencEncoder::new(&mut dup, 5_000_000) {
|
||
Ok(nv) => {
|
||
eprintln!("[capture] encoder = NVENC hardware (CBR 5Mbps@60fps, ARGB direct)");
|
||
EncoderKind::Nvenc(Box::new(nv))
|
||
}
|
||
Err(e) => {
|
||
eprintln!("[capture] NVENC unavailable ({e}); fallback to openh264 software");
|
||
match Encoder::with_api_config(
|
||
OpenH264API::from_source(),
|
||
EncoderConfig::new()
|
||
.bitrate(BitRate::from_bps(5_000_000))
|
||
.max_frame_rate(FrameRate::from_hz(60.0))
|
||
.rate_control_mode(RateControlMode::Bitrate)
|
||
.usage_type(UsageType::ScreenContentRealTime)
|
||
.intra_frame_period(IntraFramePeriod::from_num_frames(
|
||
INTRA_PERIOD_FRAMES,
|
||
))
|
||
.num_threads(4),
|
||
) {
|
||
Ok(e) => {
|
||
eprintln!("[capture] encoder = openh264 software (5Mbps@60fps, 4T)");
|
||
EncoderKind::Soft(Box::new(e))
|
||
}
|
||
Err(e) => {
|
||
eprintln!("[capture] encoder init failed: {e}");
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 最近一帧的**裸载荷**与关键帧标志:静止重发时要用它们重建帧头,
|
||
// 不能直接重发整帧(那样 seq/ts_ms 会停在上次发送时刻)
|
||
let mut last_payload: Option<Vec<u8>> = None;
|
||
let mut last_kf = false;
|
||
let mut last_sent = Instant::now() - Duration::from_secs(10);
|
||
let min_frame = Duration::from_millis(16); // 60fps 上限
|
||
let mut scratch: Vec<u8> = Vec::new(); // 软编:行距不连续时的复用缓冲
|
||
// 帧序号:每发一帧 +1(含静止桌面的重复帧),供主控端做丢帧检测
|
||
let mut seq: u64 = 0;
|
||
// 编码耗时统计(每 120 帧输出一次均值,ms)
|
||
let mut stat_frames = 0u64;
|
||
let mut stat_convert_ms = 0u64;
|
||
let mut stat_encode_ms = 0u64;
|
||
let mut stat_skipped = 0u64; // 拥塞跳帧计数
|
||
|
||
// `$kf` 由调用点显式给出:NVENC 路径知道 `force_key`,软编路径同理。
|
||
// 不要在这里猜——首帧和 KeyFrameReq 都会产生 IDR,猜错会让解码端错过重同步点。
|
||
macro_rules! emit {
|
||
($payload:expr, $kf:expr) => {{
|
||
let header = VideoFrameHeader {
|
||
width: dup.width,
|
||
height: dup.height,
|
||
ts_ms: start.elapsed().as_millis() as u64,
|
||
seq,
|
||
keyframe: $kf,
|
||
};
|
||
let payload_vec: Vec<u8> = $payload;
|
||
let frame = encode_video_frame(&header, &payload_vec);
|
||
if tx.send(Some(frame.clone())).is_err() {
|
||
return; // 会话结束
|
||
}
|
||
last_payload = Some(payload_vec);
|
||
last_kf = $kf;
|
||
last_sent = Instant::now();
|
||
seq = seq.wrapping_add(1);
|
||
stat_frames += 1;
|
||
}};
|
||
}
|
||
|
||
loop {
|
||
let iter_start = Instant::now();
|
||
match dup.acquire(80) {
|
||
Ok(true) => {
|
||
// 拥塞反馈:发送侧排队深 → 跳过本帧(NVENC 少输入一帧,参考链连续;
|
||
// 解码端收到的帧序 = 编码端实际输出序,无花屏风险)
|
||
//
|
||
// ⚠️ 必须放在消费 keyreq **之前**:先前顺序是「先 swap 再判断拥塞」,
|
||
// 一旦本帧因拥塞被跳过,已消费的 KeyFrameReq 就随 continue 一起丢失,
|
||
// 解码端等不到 IDR(与下方静止分支同源的缺陷)。
|
||
if congestion.swap(false, Ordering::Relaxed) {
|
||
stat_skipped += 1;
|
||
let el = iter_start.elapsed();
|
||
if el < min_frame {
|
||
std::thread::sleep(min_frame - el);
|
||
}
|
||
continue;
|
||
}
|
||
// 主控端请求关键帧(解码异常恢复)。走到这里一定会编码,
|
||
// 因此可以安全地消费标志。
|
||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||
match &mut encoder {
|
||
EncoderKind::Nvenc(enc) => {
|
||
let enc_start = Instant::now();
|
||
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
|
||
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||
match r {
|
||
Ok(payload) => {
|
||
let kf = h264_contains_idr(&payload);
|
||
emit!(payload, kf)
|
||
}
|
||
Err(e) => eprintln!("[capture] nvenc encode failed: {e}"),
|
||
}
|
||
}
|
||
EncoderKind::Soft(enc) => {
|
||
if force_key {
|
||
enc.force_intra_frame();
|
||
}
|
||
// 映射窗口内直接做 BGRA→YUV(行距连续时零拷贝,否则复用 scratch)
|
||
let conv_start = Instant::now();
|
||
let dims = (dup.width as usize, dup.height as usize);
|
||
let conv = dup.with_mapped(|src, pitch| {
|
||
if pitch == dims.0 * 4 {
|
||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(src, dims))
|
||
} else {
|
||
scratch.resize(dims.0 * dims.1 * 4, 0);
|
||
for (row, dst) in
|
||
scratch.chunks_exact_mut(dims.0 * 4).enumerate()
|
||
{
|
||
dst.copy_from_slice(
|
||
&src[row * pitch..row * pitch + dims.0 * 4],
|
||
);
|
||
}
|
||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(&scratch, dims))
|
||
}
|
||
});
|
||
match conv {
|
||
Ok(yuv) => {
|
||
stat_convert_ms +=
|
||
conv_start.elapsed().as_millis() as u64;
|
||
let enc_start = Instant::now();
|
||
let r = enc.encode(&yuv);
|
||
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||
match r {
|
||
Ok(ebs) => {
|
||
let payload = ebs.to_vec();
|
||
let kf = h264_contains_idr(&payload);
|
||
emit!(payload, kf)
|
||
}
|
||
Err(e) => {
|
||
eprintln!("[capture] encode failed: {e}");
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
eprintln!("[capture] frame convert failed: {e}");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if stat_frames > 0 && stat_frames % 120 == 0 {
|
||
eprintln!(
|
||
"[capture] avg convert {}ms, encode {}ms/frame, skipped {} (last 120)",
|
||
stat_convert_ms / 120,
|
||
stat_encode_ms / 120,
|
||
stat_skipped
|
||
);
|
||
stat_convert_ms = 0;
|
||
stat_encode_ms = 0;
|
||
stat_skipped = 0;
|
||
}
|
||
// 帧间隔节流到 ~60fps
|
||
let el = iter_start.elapsed();
|
||
if el < min_frame {
|
||
std::thread::sleep(min_frame - el);
|
||
}
|
||
}
|
||
Ok(false) => {
|
||
// 桌面静止:低频重发(丢包后恢复 + 会话保活)。
|
||
//
|
||
// ⚠️ 这里曾有三个缺陷,全部围绕「KeyFrameReq 被静默丢弃」:
|
||
// 1. `swap` 写在时间闸**外面**:请求若落在距上次发送 500ms 之内,
|
||
// 标志已被消费但分支不进入,该次请求直接丢失(手册 §6.1 第 3 条)。
|
||
// 2. 软编分支只重发旧码流、不看 force_key:永远吐不出 IDR(手册 §6.1 第 2 条)。
|
||
// 3. NVENC 分支虽然把 force_key 传给了编码器,但受第 1 条限制。
|
||
//
|
||
// 现在的语义:**关键帧请求绕过 500ms 节流**——它是错误恢复路径,
|
||
// 延迟一次 IDR 的代价是解码端持续黑屏,远高于多发一帧的带宽成本。
|
||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||
let due = force_key || last_sent.elapsed() > Duration::from_millis(500);
|
||
if due {
|
||
match &mut encoder {
|
||
EncoderKind::Nvenc(enc) => {
|
||
// 重编当前 GPU 帧(内容未变,输出极小;force_key 时为 IDR)
|
||
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
|
||
match r {
|
||
Ok(payload) => {
|
||
let kf = h264_contains_idr(&payload);
|
||
emit!(payload, kf)
|
||
}
|
||
Err(e) => {
|
||
eprintln!("[capture] nvenc idle encode failed: {e}")
|
||
}
|
||
}
|
||
}
|
||
EncoderKind::Soft(enc) => {
|
||
// 软编:静止桌面没有新像素可编,只能重发最近一帧。
|
||
// 但 force_key 时必须真的重编出 IDR —— 重发旧 P 帧对
|
||
// 已经解不动的解码端毫无帮助。
|
||
//
|
||
// 裸像素来源:staging 纹理里仍是上一帧内容(DXGI 只在
|
||
// 有新帧时覆盖),重新 Map 一次即可,不必每帧额外拷 14MB。
|
||
if force_key {
|
||
enc.force_intra_frame();
|
||
let dims = (dup.width as usize, dup.height as usize);
|
||
let conv = dup.with_mapped(|src, pitch| {
|
||
if pitch == dims.0 * 4 {
|
||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(
|
||
src, dims,
|
||
))
|
||
} else {
|
||
scratch.resize(dims.0 * dims.1 * 4, 0);
|
||
for (row, dst) in
|
||
scratch.chunks_exact_mut(dims.0 * 4).enumerate()
|
||
{
|
||
dst.copy_from_slice(
|
||
&src[row * pitch..row * pitch + dims.0 * 4],
|
||
);
|
||
}
|
||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(
|
||
&scratch, dims,
|
||
))
|
||
}
|
||
});
|
||
match conv {
|
||
Ok(yuv) => match enc.encode(&yuv) {
|
||
Ok(ebs) => {
|
||
let payload = ebs.to_vec();
|
||
let kf = h264_contains_idr(&payload);
|
||
emit!(payload, kf)
|
||
}
|
||
Err(e) => eprintln!(
|
||
"[capture] idle keyframe encode failed: {e}"
|
||
),
|
||
},
|
||
Err(e) => {
|
||
eprintln!("[capture] idle keyframe convert failed: {e}")
|
||
}
|
||
}
|
||
} else if let Some(p) = last_payload.as_ref() {
|
||
// 非关键帧的保活重发:复用载荷但重建帧头(seq 必须递增)
|
||
let kf = last_kf;
|
||
emit!(p.clone(), kf)
|
||
}
|
||
}
|
||
}
|
||
last_sent = Instant::now();
|
||
} else {
|
||
std::thread::sleep(Duration::from_millis(8));
|
||
}
|
||
}
|
||
Err(e) => {
|
||
eprintln!("[capture] acquire failed: {e}");
|
||
break; // ACCESS_LOST 等,重建
|
||
}
|
||
}
|
||
}
|
||
std::thread::sleep(Duration::from_millis(300));
|
||
}
|
||
}
|