I needed cross-platform screen capture in Rust, so I built pinray
I needed cross-platform screen capture in Rust, so I built pinray
我需要在 Rust 中实现跨平台屏幕录制,于是我开发了 pinray
So I needed screen capture in Rust. Simple enough, right? Wrong. I went looking for a crate and found three options: scap, xcap, and waycap-rs. Each one had problems that drove me nuts. So I open sourced pinray, a Rust crate for cross-platform screen and system audio capture. The goal is simple: provide a single API over each platform’s native capture APIs without depending on ffmpeg or another large capture framework. pinray focuses on capture only. It gives you raw video and audio frames with the metadata needed to build your own recording, streaming, or processing pipeline. Repository: https://github.com/Itz-Agasta/pinray
我需要在 Rust 中实现屏幕录制。听起来很简单,对吧?其实不然。我找遍了现有的 crate,发现了三个选项:scap、xcap 和 waycap-rs。但每一个都有让我抓狂的问题。于是,我开源了 pinray,这是一个用于跨平台屏幕和系统音频录制的 Rust crate。它的目标很简单:在各平台的原生录制 API 之上提供统一的 API,且不依赖 ffmpeg 或其他大型录制框架。pinray 专注于录制本身,它提供原始的视频和音频帧,以及构建你自己的录制、流媒体或处理流水线所需的元数据。仓库地址:https://github.com/Itz-Agasta/pinray
Why I built it?
为什么我要开发它?
xcap: xcap has the best cross-platform story, but its frame type is a joke: just width, height, and raw bytes. No stride. No pixel format. No timestamps. You’re flying blind. xcap: xcap 的跨平台支持做得最好,但它的帧类型简直是个笑话:只有宽度、高度和原始字节。没有步长(stride)、没有像素格式、没有时间戳。这简直是在盲人摸象。
scap: scap has a better frame model but it isnt maintain anymore. I got a lot of compile error due to its PipeWire 0.8.0 dependency chain on my arch. Issue #185. Its Linux engine is also one giant struct with #[cfg] fields scattered throughout, which makes extending it painful.
scap: scap 的帧模型更好,但它已经不再维护了。由于它对 PipeWire 0.8.0 的依赖链,我在 Arch Linux 上遇到了大量的编译错误(Issue #185)。它的 Linux 引擎也是一个巨大的结构体,里面散布着各种 #[cfg] 字段,这使得扩展它变得非常痛苦。
waycap-rs: waycap-rs is probably the strongest Wayland implementation. But it’s Wayland-only. And it shells out to pactl for audio device discovery. In a library. In 2026.
waycap-rs: waycap-rs 可能是最强大的 Wayland 实现,但它仅支持 Wayland。而且它竟然在库代码里通过调用 pactl 来发现音频设备。这可是 2026 年了。
None of them gave me what I actually wanted: A clean separation between capture backends and the public API; A frame model with enough metadata to do real work. 它们都没有提供我真正想要的东西:录制后端与公共 API 之间的清晰分离,以及包含足够元数据以进行实际工作的帧模型。
What pinray does
pinray 的功能
pinray is a capture infrastructure crate. It talks directly to each operating system’s native capture APIs and delivers raw frames together with their metadata, including timestamps, pixel format, stride, sequence numbers, and dropped-frame notifications. Encoding is intentionally out of scope. The output can be sent to ffmpeg, WebRTC, wgpu, the image crate, or any custom processing pipeline. pinray 是一个录制基础设施 crate。它直接与各操作系统的原生录制 API 通信,并提供原始帧及其元数据,包括时间戳、像素格式、步长、序列号和丢帧通知。编码功能被刻意排除在范围之外。输出的数据可以发送到 ffmpeg、WebRTC、wgpu、image crate 或任何自定义处理流水线。
The current native backends are: 目前支持的原生后端:
| Platform | Video | Audio |
|---|---|---|
| Linux (Wayland) | XDG Desktop Portal + PipeWire | PipeWire |
| Linux (X11) | XGetImage polling | PipeWire |
| macOS 12.3+ | ScreenCaptureKit | ScreenCaptureKit |
| Windows 10+ | DXGI Desktop Duplication + Windows Graphics Capture | WASAPI Loopback |
No wrapper crates are used around these platform APIs. 这些平台 API 没有使用任何包装器 crate。
Basic usage
基本用法
Capturing the primary display together with system audio looks like this: 录制主显示器和系统音频的代码如下:
use std::time::Duration;
use pinray::{AudioCapture, CaptureEvent, CaptureSession, SourceId, VideoCaptureTarget};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut session = CaptureSession::builder()
.video_target(VideoCaptureTarget::Display(SourceId::new("auto")))
.audio(AudioCapture::SystemMix)
.build()?;
session.start()?;
loop {
match session.next_event(Some(Duration::from_secs(5)))? {
CaptureEvent::Video(frame) => {
println!("{}x{}", frame.width, frame.height);
}
CaptureEvent::Audio(frame) => {
println!("{} Hz", frame.sample_rate);
}
CaptureEvent::Gap(gap) => {
println!("Dropped frames: {:?}", gap.reason);
}
CaptureEvent::End => break,
}
}
session.stop()?;
Ok(())
}
The same API works across Linux, macOS, and Windows. On Linux, Auto selects either the Wayland or X11 backend. On Windows it prefers DXGI Desktop Duplication and falls back to Windows Graphics Capture when appropriate. backend_info() can be used to inspect which backend was selected.
同一套 API 适用于 Linux、macOS 和 Windows。在 Linux 上,Auto 会自动选择 Wayland 或 X11 后端。在 Windows 上,它优先使用 DXGI Desktop Duplication,并在必要时回退到 Windows Graphics Capture。可以使用 backend_info() 来查看选择了哪个后端。
Window enumeration follows the same API: 窗口枚举也遵循相同的 API:
use pinray::{CaptureSource, CaptureSession};
let sources = pinray::enumerate_sources()?;
let window = sources.iter().find_map(|source| match source {
CaptureSource::Window(window) if window.title.contains("Firefox") => {
Some(window.id.clone())
}
_ => None,
});
Audio-only capture is equally straightforward: 仅录制音频同样简单:
let mut session = CaptureSession::builder()
.audio(AudioCapture::SystemMix)
.build()?;
Design decisions
设计决策
The most challenging part was not implementing screen capture for a single platform. It was exposing a consistent API over several fundamentally different capture systems. 最困难的部分不是为单个平台实现屏幕录制,而是在几个本质上完全不同的录制系统之上提供一致的 API。
Different capture models: Each platform delivers frames differently. ScreenCaptureKit and Windows Graphics Capture continuously stream frames. DXGI Desktop Duplication only produces new frames when the desktop changes, so an idle desktop naturally results in timeouts. X11 has no event-driven capture API, so the backend polls at the requested frame rate. 不同的录制模型:每个平台交付帧的方式都不同。ScreenCaptureKit 和 Windows Graphics Capture 是持续流式传输帧的。DXGI Desktop Duplication 仅在桌面发生变化时才产生新帧,因此桌面空闲时自然会导致超时。X11 没有事件驱动的录制 API,因此后端会以请求的帧率进行轮询。
Consistent timestamps: Every platform uses a different clock. Windows exposes QPC ticks, macOS uses host time, and PipeWire exposes different timing information. pinray normalizes everything to monotonic nanosecond timestamps so audio and video from the same capture session share a common timeline. 一致的时间戳:每个平台使用不同的时钟。Windows 使用 QPC 滴答数,macOS 使用主机时间,PipeWire 则暴露不同的计时信息。pinray 将所有内容标准化为单调纳秒时间戳,以便来自同一录制会话的音频和视频共享一个共同的时间轴。
Wayland support: Wayland intentionally requires user approval through the desktop portal. Instead of trying to work around that, pinray embraces the portal workflow. SourceId::new("auto") opens the native picker, while restore tokens allow subsequent sessions to reuse previously granted permissions.
Wayland 支持:Wayland 特意要求通过桌面门户(desktop portal)获得用户批准。pinray 没有试图绕过这一点,而是拥抱了门户工作流。SourceId::new("auto") 会打开原生选择器,而恢复令牌(restore tokens)允许后续会话重用之前授予的权限。
Native pixel formats: All current backends produce BGRA frames. If an application requests RGBA, pinray performs the conversion explicitly. There are no hidden format conversions. 原生像素格式:当前所有后端都生成 BGRA 帧。如果应用程序请求 RGBA,pinray 会显式执行转换。没有任何隐藏的格式转换。
Frame drops: Dropped frames are exposed as explicit Gap events, and every frame carries a sequence number. Applications that synchronize audio and video need this information, so it is surfaced directly instead of being hidden internally.
丢帧处理:丢帧被显式暴露为 Gap 事件,并且每一帧都带有序列号。需要同步音视频的应用程序需要这些信息,因此这些信息被直接呈现出来,而不是在内部隐藏。
Getting started
开始使用
cargo add pinray
Linux requires libpipewire-0.3-dev and clang during compilation. No additional dependencies are required on macOS or Windows.
Linux 在编译时需要 libpipewire-0.3-dev 和 clang。macOS 或 Windows 上不需要额外的依赖。
Repository: https://github.com/Itz-Agasta/pinray Documentation: https://docs.rs/pinray Crate: https://crates.io/crates/pinray
If you try pinray and run into an issue, feel free to open one on GitHub. Including the output of session.backend_info() is especially helpful since backend selection can differ between systems.
如果你尝试使用 pinray 并遇到问题,欢迎在 GitHub 上提交 Issue。提供 session.backend_info() 的输出非常有帮助,因为不同系统间的后端选择可能会有所不同。