//! 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 { unsafe { let mut device: Option = None; let mut context: Option = 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 = 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 = 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 { unsafe { let mut info = DXGI_OUTDUPL_FRAME_INFO::default(); let mut resource: Option = 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(&mut self, f: impl FnOnce(&[u8], usize) -> R) -> 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 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 { // 打开会话(无 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> { // 首帧强制 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>>, keyreq: Arc, congestion: Arc) { 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), 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] 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> = 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 = 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 = $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)); } }