From 41093a4f990965e6335af3982a75e2164f620470 Mon Sep 17 00:00:00 2001 From: dailz Date: Mon, 22 Jun 2026 18:33:42 +0800 Subject: [PATCH] =?UTF-8?q?docs(avhw):=20[4/4]=20=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=20SwEncState=20=E4=B8=8E=20unsafe=20impl=20S?= =?UTF-8?q?end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/avhw.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/avhw.rs b/src/avhw.rs index 4ae445a..6da3346 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -1922,15 +1922,28 @@ impl Drop for SwEncEncode { } } +// 软件编码复合状态:把"GPU 辅助导入/缩放半部"(SwEncImport)与"CPU 软编码半部" +// (SwEncEncode)拼成一个对外暴露的单一类型。调用方只看 SwEncState,不直接接触 +// 两个内部组件——类似 Go 中把两个 struct 组合成上层 API 对象。 +// 字段语义: +// - import:管理 DRM/VAAPI 设备 + scale_vaapi 滤镜图,把 BGRA 硬件帧下采样为 NV12 +// - encode:管理 libx264/libopenh264 编码器 + 可选 muxer,消费 NV12 输出 H.264 +// 数据流:CaptureSource → import.import_and_scale(hw_frame) → encode.encode_cpu_frame(nv12) pub struct SwEncState { import: SwEncImport, encode: SwEncEncode, } +// 中文概述(上方):SwEncState 内部含裸指针 C 资源(VAAPI surfaces、AVCodecContext、 +// AVFormatContext 等),Rust 默认不为含裸指针的类型实现 Send。这里手工声明 Send 表明 +// **本工程的并发模型是"单线程独占"**——所有 FFI 调用都通过 &mut self 串行化,跨线程 +// 移动只发生在外部序列化点(main.rs 中的编码线程独占)。AGENTS.md exclusivity 警告适用。 // SAFETY: SwEncState owns import and encode state exclusively and existing sync callers move it // between threads only with external serialization; all FFI handles are accessed through &mut self. unsafe impl Send for SwEncState {} +// SwEncState 实现:4 个 pub 方法按数据流顺序——new/new_webrtc 构造、frames_rgb +// 暴露硬件帧池给上游采集器、encode_frame 走单帧流水线、flush 处理 EOF。 impl SwEncState { #[allow(clippy::too_many_arguments)] pub fn new( @@ -1944,12 +1957,16 @@ impl SwEncState { bitrate: u64, gop_size: u32, ) -> Result { + // 文件输出模式构造:上游采集源分辨率 width×height,缩放到 enc_width×enc_height + // 后送入 libx264/libopenh264;bitrate/gop_size 由调用方计算(参见 create_encoder)。 + // `?` 自动传播 anyhow Error(同 Go `if err != nil { return err }`)。 tracing::info!( "SwEncState::new: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264" ); let import = SwEncImport::new(drm_device, width, height, enc_width, enc_height, fps)?; let encode = SwEncEncode::new_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?; + // Self 是 impl 块当前类型的别名;struct literal 字段简写(同字段名变量直接写名字)。 Ok(Self { import, encode }) } @@ -1966,10 +1983,16 @@ impl SwEncState { tx: crossbeam_channel::Sender, webrtc_paused: Arc, ) -> Result { + // WebRTC 模式构造:H.264 NALU 通过 crossbeam channel 推给 str0m 信令线程(见 webrtc.rs)。 + // 区别于 new:用 channel 代替 muxer;webrtc_paused 是 str0m ICE/DTLS 暂停标志(Arc + // 跨线程共享,Ordering::Relaxed 语义详见 state_portal.rs/T9b 注释)。 tracing::info!( "SwEncState::new_webrtc: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264 -> WebRTC" ); let import = SwEncImport::new(drm_device, width, height, enc_width, enc_height, fps)?; + // dummy channel idiom:创建后立即 drop 发送端,使接收端永远返回 Disconnected—— + // 等价于"该信号源永不触发",编码器内部的 bitrate/resolution 切换逻辑因此走默认路径。 + // 类比 Go:`ch := make(chan T, 1); close(ch)` 让 `<-ch` 立即返回零值(但语义不同)。 let (dummy_tx, bitrate_rx) = crossbeam_channel::bounded(1); drop(dummy_tx); let (dummy_resolution_tx, resolution_rx) = crossbeam_channel::bounded(1); @@ -1989,15 +2012,21 @@ impl SwEncState { } pub fn frames_rgb(&self) -> &AvHwFrameCtx { + // 借用访问器:返回 import 持有的硬件帧池引用。借用检查器保证调用方在持有 + // 这个 &AvHwFrameCtx 期间无法调 encode_frame(&mut self),避免数据竞争。 self.import.frames_rgb() } pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> { + // 单帧主循环:把硬件 BGRA 帧导入 VAAPI → scale_vaapi 下采样到 NV12 → transfer + // 到 CPU → libx264 编码。&mut self 独占借用保证本调用期间不会并发访问 import/encode。 let cpu_frame = self.import.import_and_scale(hw_frame)?; self.encode.encode_cpu_frame(&cpu_frame).map(|_| ()) } pub fn flush(&mut self) -> Result<()> { + // EOF 处理 3 步:(1) 排空 import 中残留的 scale_vaapi 滤镜帧;(2) encode.flush() + // 给编码器送 NULL frame 触发 EOS drain;(3) write_trailer_if_needed 仅 muxer 模式生效。 for frame in self.import.flush_import()? { self.encode.encode_cpu_frame(&frame)?; } @@ -2010,6 +2039,9 @@ impl SwEncState { // Shared encoder creation (used by both wlr-screencopy and portal paths) // --------------------------------------------------------------------------- +// 中文概述(上方):本函数是 wlr-screencopy / Portal 两条采集路径共享的 EncState +// 构造器,封装了 bitrate/GOP 默认值计算 + 旋转输出维度的转置(transform)。 +// 英文 /// block 保留不动。 /// Create a fully configured encoder with VAAPI hardware acceleration. /// /// Convenience wrapper around [`EncState::new`] that computes default values @@ -2027,9 +2059,14 @@ pub fn create_encoder( gop_size: Option, existing_hw_ctx: Option, ) -> Result { + // transform 决定编码方向:90°/270° 旋转时宽高对调(transpose_if_transform_transposed + // 见 transform.rs)。Option ↔ Go `*T`,必须显式处理 None 分支。 let (enc_w, enc_h) = transpose_if_transform_transposed(transform, width as i32, height as i32); + // bitrate 默认值 = 2*W*H*fps/100(约 0.02 bits/pixel/frame,对应 H.264 中等质量)。 + // unwrap_or_else 是延迟构造:闭包仅在 None 时求值(类比 Go `if x == nil { x = ... }`)。 let actual_bitrate = bitrate.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100); + // GOP 默认 = 1 秒(fps 个帧),平衡 IDR 刷新频率与压缩率。 let actual_gop_size = gop_size.unwrap_or(fps); EncState::new( drm_device, @@ -2060,7 +2097,12 @@ fn build_swenc_filter_graph( enc_height: u32, fps: u32, ) -> Result { + // 构造软件编码用的 GPU scale_vaapi 滤镜图:BGRA 硬件帧 → VAAPI 下采样 → NV12。 + // 这是 SwEncImport 的核心:保留 GPU 做缩放/色彩转换(CPU 不能高效处理 4K BGRA)。 let mut graph = ff::filter::Graph::new(); + // 通过名称查找 FFmpeg 滤镜(buffer=源、buffersink=汇、scale_vaapi=VAAPI 缩放)。 + // ok_or_else 把 Option 转为 Result(None 时执行闭包构造错误),类比 Go 中 + // `if v, ok := m[k]; !ok { return err }`。 let buffersrc = ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?; let buffersink = ff::filter::find("buffersink") @@ -2070,18 +2112,25 @@ fn build_swenc_filter_graph( // FFmpeg 8.0+ rejects VAAPI pix_fmt in buffer args before hw_frames_ctx is attached. // Use a SW placeholder, then override format/hw_frames_ctx with av_buffersrc_parameters_set. + // 中文补充:FFmpeg 8 起对 buffer 滤镜参数加了严格校验——args 字符串里写 pix_fmt=vaapi + // 会在 attach hw_frames_ctx 之前就被拒绝。workaround 是先用 bgra 占位构造 src_ctx, + // 然后用 av_buffersrc_parameters_set 覆盖真实 format/hw_frames_ctx。 let args = format!( "video_size={}x{}:pix_fmt=bgra:time_base=1/{fps}:pixel_aspect=1/1", width, height, ); let mut src_ctx = graph.add(&buffersrc, "in", &args)?; + // 中文概述(unsafe):分配 AVBufferSrcParameters 结构体——FFmpeg C API 返回新分配的 + // 内存指针(需配对 av_free)。`is_null()` 检查后才能解引用。 // SAFETY: av_buffersrc_parameters_alloc returns newly allocated parameters // or null, which is checked below. let par = unsafe { ffi::av_buffersrc_parameters_alloc() }; if par.is_null() { bail!("av_buffersrc_parameters_alloc returned null"); } + // 中文概述(unsafe):把 VAAPI format / 尺寸 / 时基 / hw_frames_ctx 写入 par,再 set 给 src_ctx。 + // ref_clone 增加 AVBufferRef 引用计数(FFmpeg 共享硬件帧池的标准方式)。 // SAFETY: par and src_ctx are valid; frames_rgb.ref_clone returns an owned hw_frames_ctx ref // that buffersrc consumes on successful parameter set. unsafe { @@ -2105,12 +2154,15 @@ fn build_swenc_filter_graph( "scale", &format!("{enc_width}:{enc_height}:format=nv12"), )?; + // 中文概述(unsafe):scale_vaapi 滤镜需要 hw_device_ctx 才能访问 VAAPI 设备。 + // ref_clone 共享 hw_dev 的 AVHWDeviceContext,滤镜图存活期间引用计数 > 0。 // SAFETY: scale_vaapi keeps a ref-counted device context while the graph is alive. unsafe { (*scale_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone(); } let mut sink_ctx = graph.add(&buffersink, "out", "")?; + // 链接滤镜图:src(0) → scale(0) → sink(0),端口号 0 表示第 0 个输出/输入 pad。 src_ctx.link(0, &mut scale_ctx, 0); scale_ctx.link(0, &mut sink_ctx, 0); graph @@ -2121,6 +2173,9 @@ fn build_swenc_filter_graph( } fn create_nv12_to_yuv420p_sws(width: u32, height: u32) -> Result<*mut ffi::SwsContext> { + // 创建 FFmpeg 软件色彩转换器:NV12 → YUV420P,同尺寸无缩放(仅把 NV12 的 interleaved + // UV 半平面拆为 YUV420P 的 planar UV 两个半平面)。返回裸指针(调用方持有所有权, + // 必须配对 sws_freeContext——见 SwEncEncode::recreate_encoder/Drop)。 // SAFETY: sws_getContext creates an owned scaler context for same-size NV12 -> YUV420P. let ctx = unsafe { ffi::sws_getContext( @@ -2143,6 +2198,7 @@ fn create_nv12_to_yuv420p_sws(width: u32, height: u32) -> Result<*mut ffi::SwsCo } fn alloc_yuv420p_frame(width: u32, height: u32) -> Result<*mut ffi::AVFrame> { + // 分配一个 YUV420P AVFrame 并分配其可写缓冲区。返回裸指针——调用方负责 av_frame_free。 // SAFETY: Allocate an AVFrame, configure format/dimensions, then allocate writable buffers. unsafe { let mut frame = ffi::av_frame_alloc(); @@ -2154,6 +2210,7 @@ fn alloc_yuv420p_frame(width: u32, height: u32) -> Result<*mut ffi::AVFrame> { (*frame).format = ffi::AVPixelFormat::AV_PIX_FMT_YUV420P as i32; let ret = ffi::av_frame_get_buffer(frame, 0); if ret < 0 { + // 失败路径必须先释放已分配的 frame,避免内存泄漏。 ffi::av_frame_free(&mut frame); bail!("av_frame_get_buffer failed: {}", ff_err(ret)); } @@ -2172,7 +2229,12 @@ fn create_software_h264_muxer( ff::codec::encoder::video::Video, ff::format::context::Output, )> { + // 文件 muxer 模式的软件 H.264 编码器构造。返回 (enc_video, octx) 元组—— + // Rust 元组解构返回,类比 Go 的 multiple return values。 + // CString 是 FFI 桥梁:FFmpeg C API 需要 NUL 终止字符串;to_str().unwrap() 假设 UTF-8。 let output_cstr = CString::new(output_path.to_str().unwrap())?; + // 优先 libx264(更高压缩率),fallback libopenh264(纯 C++ 实现,无 GPL 限制)。 + // or_else + ok_or_else 三层链式:先尝试 A → 失败尝试 B → 都失败构造错误。 let codec = ff::encoder::find_by_name("libx264") .or_else(|| ff::encoder::find_by_name("libopenh264")) .ok_or_else(|| { @@ -2180,6 +2242,8 @@ fn create_software_h264_muxer( })?; let codec_name = codec.name().to_string(); + // 块表达式(block expression)求值为最后一个表达式(ctx.encoder().video()?), + // 类比 Go IIFE:用 {} 创建临时作用域隔离 ctx,只保留 enc 借用。 let mut enc = { let ctx = ff::codec::Context::new_with_codec(codec); ctx.encoder().video()? @@ -2190,14 +2254,20 @@ fn create_software_h264_muxer( enc.set_bit_rate(bitrate as usize); enc.set_gop(gop_size); enc.set_time_base(ff::Rational::new(1, fps as i32)); + // B-frame = 双向预测帧,提高压缩率但增加延迟(max_b_frames=3 适合离线 muxer, + // 不适合 WebRTC,见 create_software_h264_encoder)。 enc.set_max_b_frames(3); + // 中文概述(unsafe):MP4/mkv 容器需要 codec global header(SPS/PPS 在 extradata + // 而不是每个 IDR),其他 muxer 无副作用。 // SAFETY: global headers are needed by MP4 and harmless for other common muxers. unsafe { (*enc.as_mut_ptr()).flags |= ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32; } if codec_name == "libx264" { + // 中文概述(unsafe):通过 av_opt_set 设置 libx264 私有选项(preset/threads)。 + // 每个 CString 仅在对应 av_opt_set 调用内存活——FFmpeg 在调用内复制字符串。 // SAFETY: priv_data and codec context belong to the unopened encoder; // strings live for each av_opt_set call. unsafe { @@ -2215,11 +2285,14 @@ fn create_software_h264_muxer( } } + // 打开编码器:open() 消耗 enc,返回 (Video, ...) 元组。 let opened = enc .open() .map_err(|e| anyhow::anyhow!("Failed to open {codec_name} encoder: {e}"))?; let enc_video = opened.0; + // 路径含 "null" 时用 null muxer(丢弃所有输出,用于基准/调试)。 + // map + unwrap_or 链式处理 Option<&str>,类比 Go `if s, ok := p.to_str(); ok { ... }`。 let use_null = output_path .to_str() .map(|s| s.contains("null")) @@ -2236,6 +2309,8 @@ fn create_software_h264_muxer( }; let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut(); + // 中文概述(unsafe):分配输出 AVFormatContext。FFmpeg 根据 output_path 后缀 + // 自动推断 muxer(.mp4 → mp4 muxer,.mkv → matroska),或用 fmt_name 强制。 // SAFETY: fmt_ctx_ptr is initialized by FFmpeg; C strings live across the call. let ret = unsafe { ffi::avformat_alloc_output_context2( @@ -2249,23 +2324,29 @@ fn create_software_h264_muxer( bail!("Failed to allocate output format context: {}", ff_err(ret)); } + // 中文概述(unsafe):在 fmt_ctx 中新建一个 stream 容器(默认空 codecpar)。 // SAFETY: fmt_ctx_ptr is valid; stream and codec parameters are owned by the format context. let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) }; if stream_ptr.is_null() { bail!("Failed to create output stream"); } + // 中文概述(unsafe):从 encoder context 复制 codec 参数(分辨率/profile/level/extradata) + // 到 stream->codecpar,让 muxer 写入容器头。 // SAFETY: stream_ptr and encoder context are valid; parameters are copied into stream. let ret = unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) }; if ret < 0 { bail!("Failed to copy codec parameters to stream: {}", ff_err(ret)); } + // 中文概述(unsafe):stream 的 time_base 取自 encoder,保持 PTS 单位一致。 // SAFETY: stream_ptr is valid and writable during muxer setup. unsafe { (*stream_ptr).time_base = (*enc_video.as_ptr()).time_base; } + // 中文概述(unsafe):对需要文件的 muxer(非 null muxer)打开 AVIO 写句柄。 + // null muxer 设置 AVFMT_NOFILE 标志,跳过 avio_open。 // SAFETY: open an AVIO only for muxers that require files; null muxer advertises NOFILE. unsafe { if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 { @@ -2284,12 +2365,15 @@ fn create_software_h264_muxer( } } + // 中文概述(unsafe):写容器头(MP4 ftyp/moov atom、mkv EBML header 等)。 // SAFETY: fmt_ctx_ptr is fully configured. let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) }; if ret < 0 { bail!("Failed to write output header: {}", ff_err(ret)); } + // 中文概述(unsafe):把裸 fmt_ctx_ptr 包装回 ffmpeg-next 的 Output 类型, + // 之后由 Rust 端管理生命周期——Drop 时调用 av_write_trailer + avformat_free_context。 // SAFETY: ownership of fmt_ctx_ptr transfers to ffmpeg-next Output wrapper. let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) }; tracing::info!("Using software H.264 encoder: {codec_name}"); @@ -2303,6 +2387,11 @@ fn create_software_h264_encoder( bitrate: u64, gop_size: u32, ) -> Result { + // WebRTC 模式的软件 H.264 编码器构造(仅返回 encoder,不带 muxer)。关键差异: + // - time_base = 1/90000(RTP 时钟单位),不是 1/fps + // - max_b_frames = 0(B 帧会破坏 RTP 实时性) + // - preset = veryfast + tune = zerolatency(最低延迟) + // - forced-idr + repeat_headers(IDR 内联 SPS/PPS,WebRTC 浏览器需要) let codec = ff::encoder::find_by_name("libx264") .or_else(|| ff::encoder::find_by_name("libopenh264")) .ok_or_else(|| anyhow::anyhow!("No H.264 software encoder found"))?; @@ -2326,9 +2415,13 @@ fn create_software_h264_encoder( // libx264 infers wrong fps from the 90kHz time_base and VBV rate control // breaks. Per Oracle review round for #25. enc.set_frame_rate(Some(ff::Rational::new(fps as i32, 1))); + // 关键:WebRTC 不允许 B 帧——B 帧需要"未来帧"参考,但 RTP 是顺序发送。 enc.set_max_b_frames(0); if codec_name == "libx264" { + // 中文概述(unsafe):通过 av_opt_set 设置 libx264 私有选项(preset/tune/threads/ + // forced-idr/x264opts)。每个 CString 仅在对应 av_opt_set 调用内存活——FFmpeg 在调用 + // 内部复制字符串。tune=zerolatency 关闭所有缓冲(sync-lookahead=0, rc-lookahead=0)。 // SAFETY: priv_data and codec context belong to the unopened encoder; // each CString lives for the duration of its av_opt_set call. unsafe { @@ -2393,8 +2486,12 @@ fn build_filter_graph( fps: u32, transform: Transform, ) -> Result { + // 硬件 VAAPI 路径的滤镜图(EncState 用,与 build_swenc_filter_graph 区别是不下采样): + // src(BGRA hw) → scale_vaapi(原尺寸 + NV12 转换) → [transpose_vaapi(如非 Normal)] → sink。 + // transform 决定是否插入 transpose_vaapi,8 个 Transform variant 各对应一个 dir 值。 let mut graph = ff::filter::Graph::new(); + // 同 build_swenc_filter_graph:通过名称查找三个核心滤镜。 let buffersrc = ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?; let buffersink = ff::filter::find("buffersink") @@ -2403,6 +2500,8 @@ fn build_filter_graph( .ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?; // buffersrc — use AVBufferSrcParameters to set hw_frames_ctx properly + // 中文补充:这里不像 build_swenc_filter_graph 那样用 bgra 占位——直接在 args 里 + // 写 pix_fmt=VAAPI。实际行为相同(都被后续 av_buffersrc_parameters_set 覆盖)。 let args = format!( "video_size={}x{}:pix_fmt={}:time_base=1/{fps}:pixel_aspect=1/1", width, @@ -2411,11 +2510,13 @@ fn build_filter_graph( ); let mut src_ctx = graph.add(&buffersrc, "in", &args)?; + // 中文概述(unsafe):同 build_swenc_filter_graph,分配 AVBufferSrcParameters。 // SAFETY: av_buffersrc_parameters_alloc allocates params for the buffersrc. let par = unsafe { ffi::av_buffersrc_parameters_alloc() }; if par.is_null() { bail!("av_buffersrc_parameters_alloc returned null"); } + // 中文概述(unsafe):把 VAAPI hw_frames_ctx 附加到 src,让 scale_vaapi 能访问硬件帧池。 // SAFETY: Set hw_frames_ctx on the buffersrc parameters, then apply. unsafe { (*par).format = Into::::into(ff::format::Pixel::VAAPI) as i32; @@ -2434,6 +2535,8 @@ fn build_filter_graph( } // scale_vaapi: hardware scaling and colourspace conversion (keeps original dimensions) + // 中文补充:与 build_swenc_filter_graph 不同,这里 scale 输出 = 输入尺寸(width:height), + // 仅做 BGRA→NV12 颜色空间转换,不做下采样。 let mut scale_ctx = graph.add( &scale_vaapi, "scale", @@ -2450,13 +2553,19 @@ fn build_filter_graph( // Build filter chain: src -> scale -> [transpose] -> sink src_ctx.link(0, &mut scale_ctx, 0); + // match 穷尽性:8 个 Transform variant,Normal 走直连分支,其他 7 个走 transpose 分支。 + // `other =>` 通配符 arm 与 `Transform::Normal` 显式 arm 共存——类比 Go type switch。 match transform { Transform::Normal => { scale_ctx.link(0, &mut sink_ctx, 0); } other => { + // 非 Normal:插入 transpose_vaapi 滤镜。dir 值映射 FFmpeg transpose doc 中的 + // "clockflip" 表(0-6 对应 8 种旋转/翻转组合)。 let transpose = ff::filter::find("transpose_vaapi") .ok_or_else(|| anyhow::anyhow!("filter 'transpose_vaapi' not found"))?; + // 嵌套 match:此处 other 已知不是 Normal,但 Rust 仍要求穷尽所有 variant, + // Normal 分支用 unreachable!() 标记(运行时若触发说明 enum 扩展未更新)。 let dir_val = match other { Transform::Normal90 => "1", Transform::Normal180 => "4", @@ -2468,6 +2577,7 @@ fn build_filter_graph( Transform::Normal => unreachable!(), }; let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?; + // 中文概述(unsafe):transpose_vaapi 同样需要 hw_device_ctx 访问 VAAPI 设备。 // SAFETY: trans_ctx is a live transpose_vaapi filter context; // scale_vaapi/transpose_vaapi keep a ref-counted device context. unsafe {