Skip to content

Capture Pipeline

51hhh edited this page Sep 16, 2026 · 1 revision

截图与冻结帧

会话模型

一次截图动作只有一个所有者:CaptureSession。会话守卫保证并发的快捷键连按不会产生两个互相覆盖的会话;后端核对 session / monitor / 物理尺寸后才接受提交。

截图只有覆盖层一个窗口,没有独立的编辑器窗口。

帧获取回退链(Linux)

Wayland 上按顺序回退,任一层成功即停:

1. Mutter 的 PipeWire 屏幕流    org.gnome.Mutter.ScreenCast
2. 自带 GNOME Shell 扩展逐屏    CaptureArea,协议 v5
3. 同一扩展的整屏舞台图         兜底
4. wlroots
5. Portal(非交互)
6. GNOME
7. xcap

返回的临时文件一律由 TemporaryScreenshotFile 兜底删除。

为什么第一层是屏幕流而不是截图接口

这是本项目里归因最清楚的一次性能优化,值得完整记录。

测量:取一次冻结帧的时间绝大部分不是合成器绘制,而是合成器自己 deflate PNG。同一批像素的对照实验(拍下来再用同一个 gdk-pixbuf 重编一次)显示 4K 那块屏 1704 ms 里有 1607 ms(94%)是 PNG 编码,合成器绘制加读回只有约 100 ms。

因此两个直觉方向都是错的:

  • 「少拍像素」无效:逐屏两次 1830 ms vs 整屏一次 1900 ms,本来就没差
  • 「在扩展里绕开 PNG 直接读像素」在 GJS 上根本不成立:没有长度标注的 array<uint8> 一律是复制入参,别名实验 3 MB 直接段错误

真正的修法是换数据源:不从合成器要图片,改从 Mutter 要视频帧。org.gnome.Mutter.ScreenCast 同一用户直接可调,不经 Portal、不弹对话框、不需要扩展也不需要注销。本机双屏实测全程 280 ms(此前 1900 ms),且每块屏都是原生像素——原生分辨率与十倍速度出自同一个修改。

一个必需的细节:取流期间顶栏会闪一下录制指示,必须传 is-recording: true,否则会落到有 5 秒最短显示时间的「停止共享」胶囊上。

为什么需要自带 Shell 扩展

GNOME Wayland 下有两件事只能以扩展身份进合成器做:

  1. 没有面向普通应用的窗口几何接口
  2. Portal 截图要求「聚焦的应用」才能弹授权对话框,而快捷键触发时必然不满足

扩展的安装只由设置页显式触发,启动时只做 reconcile_on_startup,绝不擅自安装。装了要注销一次生效,升级同样要(因此有 stale 状态),卸载即时。

调试时不必每次注销:scripts/probe-shell-capture.sh 借 org.gnome.Shell.Eval 在活着的会话里量同一段取像素代码。

为什么逐屏优先、整屏兜底

整屏舞台图在混合缩放的多屏上会把低缩放那块上采样,画面发糊。逐屏是首选。

覆盖层三态

三态由选区推导,不额外存状态:

状态 条件 行为
idle 没有选区 悬停高亮可速选窗口
dragging 按住不放 框选/移动/缩放选区,或画一笔标注
editing 有选区 工具条贴在选区旁,选区仍可拖动缩放

点击空白 = 整屏,点击窗口 = 该窗口,拖动 = 自由区域。

铺满全屏的选区没有「移动」余量,所以 geometry.ts::coversBounds 让它内部的拖拽回到重新框选——否则点一下取了整屏之后就再也框不出小区域。缩放手柄不受当前工具影响,标注工具激活时仍可改框。

工具条落点由 toolbarPlacement 计算:优先贴选区下方,放不下翻到上方,都放不下压在视口底部。

底图不走 JSON

get_capture_frame 用二进制 IPC 直传原始 RGBA(payload 只带几何),前端 putImageData 铺进离屏 canvas。因此标注渲染层的底图类型是 FrameImage = HTMLImageElement | HTMLCanvasElement。

像素改回 PNG + base64 会让覆盖层出现的时间翻倍。

覆盖层未编辑时直接显示资源协议已解码的原图,第一次标注或调色时才分配合成 Canvas;双屏 4K/1600p 会话因此避免约 49 MiB 重复 Canvas 像素,并省去首帧整图复制。

每次会话由 StageTimings 记一条分段耗时日志。

提交路径

点对钩
  → 前端只交出逻辑选区 + renderer v2 操作层
  → 后端核对 session / monitor / 物理尺寸
  → 从可信冻结帧渲染完整画面
  → 通过同一 SVG viewBox 只输出选区
  → copy / save / pin

先渲染完整画面再裁选区,所以跨选区的路径与模糊邻域不会丢失,WebView 也不再上传另一份最终 PNG。

合成在 blocking worker 执行。渲染期间取消会让后续 session 认领失败,不会产生剪贴板、文件或窗口副作用。

选区翻译直接由后端裁原始像素给本地 OCR,不经过前端画布。

窗口速选

依赖 xcap::Window::all()(本质是 X11/xcb 枚举),按堆叠顺序下发候选(sort_windows_by_stacking / X11 _NET_CLIENT_LIST_STACKING)。

枚举失败或返回空时后端记 log::info,覆盖层显示 "Window picking unavailable in this session"——让 Wayland 下的退化可见,而不是静默给出空列表。

失败处理契约

截图链路的失败分类必须彼此独立,不能互相吞掉:

  • 输出失败保留编辑内容或同一产物,按已确认的副作用提供重试
  • 连续的忙错误不会解除恢复限制
  • 输出期间右键不清空选区
  • 长截图接管失败可以回到普通截图
  • 初始化失败的页面必须能退出

一个已知的平台陷阱

覆盖层窗口本身必须由合成器摆放。在多屏逐个 fullscreen_on_monitor 的循环里,任何非主线程的 GTK 调用都会 panic(GTK may only be used from the main thread),而 panic 发生在第一屏定位之前就会导致:第一屏失去正确定位,第二屏的建窗循环根本不执行。

症状是「覆盖层偏移、只有一屏能截图」。单屏无头环境和独立 GTK fixture 都覆盖不到这条路径——这类问题只能靠真实多屏桌面验收。

Clone this wiki locally