From ddbb53d41a38cdc2844fd687b920f79c6a156837 Mon Sep 17 00:00:00 2001 From: dev-team Date: Fri, 23 Oct 2026 10:18:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20NVENC=20=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=EF=BC=88GPU=20=E9=9B=B6=E6=8B=B7=E8=B4=9D=20?= =?UTF-8?q?ARGB=20=E7=9B=B4=E9=80=9A=EF=BC=8C=E8=BD=AF=E7=BC=96=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=85=9C=E5=BA=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 编码器自动选择:NVENC(H264 High/CBR 5Mbps/UltraLowLatency/无B帧/按需IDR)→ openh264 兜底 - Duplication 增加 GPU 中间纹理(B8G8R8A8 DEFAULT),NVENC register_resource_dx11 直读,颜色转换完全省略 - KeyFrameReq → encode_picture(picture_type=IDR),静止 500ms 重编码 P 帧 - windows 0.58→0.62(nvenc crate 依赖 0.62,运行时 dlopen nvEncodeAPI64.dll 零 SDK 依赖) - 实测(GTX 1650 Ti 回环):encode 41ms→4ms/帧(10x),convert 0ms,viewer 解码正常 --- Cargo.lock | 109 ++++++++++--- Cargo.toml | 2 +- agent/Cargo.toml | 1 + agent/src/capture.rs | 359 ++++++++++++++++++++++++++++++------------- 4 files changed, 348 insertions(+), 123 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa0de8b..a7e3349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "anyhow", "common", "lz4_flex", + "nvenc", "openh264", "quinn", "rcgen", @@ -44,6 +45,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bitfields" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6e59298da389bc0649c7463856b34c6e17fe542f88939426ede4436c6b1195" +dependencies = [ + "bitfields-impl", +] + +[[package]] +name = "bitfields-impl" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c044f98f86f15414668d6c8187c7e4fadab1ad2b31680f648703e0fe07c555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "thiserror", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -567,6 +589,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "nvenc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3227c3b09b1e042dab7d8d82cea77c0404e7d0c2bbb92e7e87cb319749cd20dc" +dependencies = [ + "bitfields", + "libloading", + "windows", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1512,32 +1545,54 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", - "windows-targets", ] [[package]] name = "windows-core" -version = "0.58.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", "windows-strings", - "windows-targets", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -1546,9 +1601,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -1562,22 +1617,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-result" -version = "0.2.0" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-targets", + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-result", - "windows-targets", + "windows-link", ] [[package]] @@ -1614,6 +1678,15 @@ dependencies = [ "windows_x86_64_msvc", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 4458603..ed98c0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,5 +11,5 @@ quinn = "0.11" rustls = { version = "0.23", default-features = false, features = ["ring", "std", "logging"] } rcgen = { version = "0.13", default-features = false, features = ["ring", "pem"] } lz4_flex = "0.11" -windows = "0.58" +windows = "0.62" minifb = "0.27" diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 65a85b7..6d6475d 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -12,6 +12,7 @@ rustls.workspace = true rcgen.workspace = true lz4_flex.workspace = true openh264 = { version = "0.9.9", features = ["source"] } +nvenc = "0.1.0" windows = { workspace = true, features = [ "Win32_Foundation", "Win32_Graphics_Direct3D", diff --git a/agent/src/capture.rs b/agent/src/capture.rs index 8419b3a..48c2858 100644 --- a/agent/src/capture.rs +++ b/agent/src/capture.rs @@ -1,9 +1,17 @@ //! DXGI Desktop Duplication 采集(Windows 8+)。 -//! 帧处理:BGRA → openh264 H.264 编码(屏幕内容实时档,5Mbps)→ 线格式。 +//! 编码器自动选择:NVENC 硬编(N 卡:GPU 内 BGRA→H.264 零转换零拷贝直通) +//! → openh264 软编兜底(无 N 卡/驱动异常时自动启用)。 //! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。 use anyhow::{anyhow, Result}; use common::{encode_video_frame, 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, }; @@ -19,8 +27,9 @@ use windows::Win32::Graphics::Direct3D::{ }; use windows::Win32::Graphics::Direct3D11::{ D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, - D3D11_CREATE_DEVICE_FLAG, D3D11_CPU_ACCESS_READ, D3D11_MAP, D3D11_MAP_READ, - D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, + 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::{ @@ -31,9 +40,12 @@ use windows::Win32::Graphics::Dxgi::{ use std::time::{Duration, Instant}; pub struct Duplication { - _device: ID3D11Device, + 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, @@ -47,13 +59,13 @@ impl Duplication { D3D11CreateDevice( None, D3D_DRIVER_TYPE_HARDWARE, - HMODULE::default(), + HMODULE(std::ptr::null_mut()), D3D11_CREATE_DEVICE_FLAG(0), Some(&[D3D_FEATURE_LEVEL_11_0]), D3D11_SDK_VERSION, - Some(&mut device), + Some(&raw mut device), None, - Some(&mut context), + Some(&raw mut context), ) .map_err(|e| anyhow!("D3D11CreateDevice failed: {e}"))?; let device = device.ok_or_else(|| anyhow!("no d3d device"))?; @@ -73,6 +85,7 @@ impl Duplication { 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, @@ -95,10 +108,26 @@ impl Duplication { 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 = 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: device, + device, context, staging_res, + gpu_res, + gpu_tex, dup, width, height, @@ -106,7 +135,8 @@ impl Duplication { } } - /// true=拿到新帧并已复制到 staging;false=超时无更新 + /// true=拿到新帧并已复制;false=超时无更新。 + /// 同时复制到 staging(软编读内存)与 GPU 中间纹理(NVENC 注册输入)。 pub fn acquire(&mut self, timeout_ms: u32) -> Result { unsafe { let mut info = DXGI_OUTDUPL_FRAME_INFO::default(); @@ -116,6 +146,7 @@ impl Duplication { 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) } @@ -125,27 +156,7 @@ impl Duplication { } } - /// 把 staging 纹理映射到内存并去掉行间距 - pub fn copy_frame(&mut self) -> Result> { - 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 mut out = Vec::with_capacity(w * h * 4); - for row in 0..h { - out.extend_from_slice(&src[row * pitch..row * pitch + w * 4]); - } - self.context.Unmap(&self.staging_res, 0); - Ok(out) - } - } - - /// 在映射窗口内执行回调(避免 8MB 中间拷贝)。回调参数 = (像素数据, 行距)。 - /// 映射内存仅在回调返回前有效。 + /// 在 staging 映射窗口内执行回调(软编路径)。回调参数 = (像素数据, 行距)。 pub fn with_mapped(&mut self, f: impl FnOnce(&[u8], usize) -> R) -> Result { unsafe { let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); @@ -162,6 +173,99 @@ impl Duplication { } } +/// 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 { + // 打开会话(无 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; + config.preset_cfg.gop_len = 0xffff_ffff; // 无周期 IDR,仅按需(KeyFrameReq) + 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> { + let ty = if force_idr { + 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()?; @@ -186,116 +290,163 @@ pub fn run(tx: Sender>>, keyreq: Arc) { dup.width, dup.height ); - // H.264 编码器:屏幕内容实时档 + 码率控制 + 4 线程(单线程 1080p 仅 ~20fps, - // 多线程把编码压进 33ms 预算实现稳定 30fps) - let mut encoder = 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(300)) - .num_threads(4), - ) { - Ok(e) => e, + // ---- 编码器自动选择:NVENC 硬编优先,失败落回 openh264 软编 ---- + enum EncoderKind { + Nvenc(Box), + Soft(Box), + } + 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] encoder init failed: {e}"); - return; + 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(300)) + .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; + } + } } }; - let mut last_frame: Option> = None; + let mut last_frame: Option> = None; // 软编静止重发 let mut last_sent = Instant::now() - Duration::from_secs(10); - // 编码实测 ~5ms/帧(1080p 屏幕内容实时档),60fps 只需 16.7ms 预算, - // 够用 → 帧率上限提到 60(对标 UU 60fps;带宽由 RC 压在 5Mbps 内) let min_frame = Duration::from_millis(16); // 60fps 上限 - let mut scratch: Vec = Vec::new(); // 行距不连续时的复用缓冲 + let mut scratch: Vec = Vec::new(); // 软编:行距不连续时的复用缓冲 // 编码耗时统计(每 120 帧输出一次均值,ms) let mut stat_frames = 0u64; let mut stat_convert_ms = 0u64; let mut stat_encode_ms = 0u64; + + macro_rules! emit { + ($payload:expr) => {{ + let header = VideoFrameHeader { + width: dup.width, + height: dup.height, + ts_ms: start.elapsed().as_millis() as u64, + }; + let frame = encode_video_frame(&header, &$payload); + if tx.send(Some(frame.clone())).is_err() { + return; // 会话结束 + } + last_frame = Some(frame); + last_sent = Instant::now(); + stat_frames += 1; + }}; + } + loop { let iter_start = Instant::now(); match dup.acquire(80) { Ok(true) => { // 主控端请求关键帧(解码异常恢复) let force_key = keyreq.swap(false, Ordering::Relaxed); - if force_key { - encoder.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; + match &mut encoder { + EncoderKind::Nvenc(enc) => { let enc_start = Instant::now(); - let r = encoder.encode(&yuv); + 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(ebs) => { - let payload = ebs.to_vec(); - let header = VideoFrameHeader { - width: dup.width, - height: dup.height, - ts_ms: start.elapsed().as_millis() as u64, - }; - let frame = encode_video_frame(&header, &payload); - if tx.send(Some(frame.clone())).is_err() { - return; // 会话结束 - } - last_frame = Some(frame); - last_sent = Instant::now(); - stat_frames += 1; - if stat_frames % 120 == 0 { - eprintln!( - "[capture] avg convert {}ms, encode {}ms/frame (last 120)", - stat_convert_ms / 120, - stat_encode_ms / 120 + Ok(payload) => emit!(payload), + 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], ); - stat_convert_ms = 0; - stat_encode_ms = 0; + } + 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) => emit!(ebs.to_vec()), + Err(e) => { + eprintln!("[capture] encode failed: {e}"); + } } } Err(e) => { - eprintln!("[capture] encode failed: {e}"); + eprintln!("[capture] frame convert failed: {e}"); + break; } } } - Err(e) => { - eprintln!("[capture] frame convert failed: {e}"); - break; - } } - // 帧间隔节流到 ~30fps + if stat_frames > 0 && stat_frames % 120 == 0 { + eprintln!( + "[capture] avg convert {}ms, encode {}ms/frame (last 120)", + stat_convert_ms / 120, + stat_encode_ms / 120 + ); + stat_convert_ms = 0; + stat_encode_ms = 0; + } + // 帧间隔节流到 ~60fps let el = iter_start.elapsed(); if el < min_frame { std::thread::sleep(min_frame - el); } } Ok(false) => { - // 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变) + // 桌面静止:低频重发(丢包后恢复 + 会话保活)。 + // NVENC 重编当前 GPU 帧(输出极小 P 帧);软编重发最后一帧副本。 if last_sent.elapsed() > Duration::from_millis(500) { - if let Some(f) = last_frame.as_ref() { - if tx.send(Some(f.clone())).is_err() { - return; + match &mut encoder { + EncoderKind::Nvenc(enc) => { + // 重编当前 GPU 帧(内容未变,输出极小 P 帧) + let r = enc.encode(false, start.elapsed().as_millis() as u64); + match r { + Ok(payload) => emit!(payload), + Err(e) => { + eprintln!("[capture] nvenc idle encode failed: {e}") + } + } + } + EncoderKind::Soft(_) => { + // 软编:直接重发最后一帧码流副本 + if let Some(f) = last_frame.as_ref() { + let _ = tx.send(Some(f.clone())); + } } - last_sent = Instant::now(); } + last_sent = Instant::now(); } else { std::thread::sleep(Duration::from_millis(8)); }