<div align="center">
# pinray
**Cross-platform screen and audio capture for Rust — raw frames, real metadata, native backends.**
[](https://crates.io/crates/pinray)
[](https://docs.rs/pinray)
[](https://github.com/Itz-Agasta/pinray/actions)
[](https://github.com/Itz-Agasta/pinray/blob/main/LICENSE)
[](#minimum-supported-rust-version)
[Getting Started](https://github.com/Itz-Agasta/pinray/blob/main/crates/pinray/docs/getting-started.md) •
[Platform Support](https://github.com/Itz-Agasta/pinray/blob/main/docs/platforms.md) •
[Troubleshooting](https://github.com/Itz-Agasta/pinray/blob/main/docs/troubleshooting.md) •
[API Docs](https://docs.rs/pinray) •
[Type reference (any platform)](https://docs.rs/pinray-core)
<img
alt="banner"
src="https://github.com/user-attachments/assets/f8744ece-3aff-4c72-8fa8-add76d571280"
/>
</div>
---
pinray is a capture **infrastructure** crate: it talks to each OS's native capture API and hands you raw video and audio frames with their metadata intact - stride, pixel format, timestamps, sequence numbers, drop signals. Encoding is deliberately out of scope; pipe frames into ffmpeg, WebRTC, wgpu, image crates, or your own pipeline.
## Why pinray
- **Native backends, no wrapper crates** - XDG Desktop Portal + PipeWire and X11 on Linux, ScreenCaptureKit (via objc2) on macOS, DXGI Desktop Duplication + Windows Graphics Capture + WASAPI (via the `windows` crate) on Windows.
- **Audio is first-class** - system-mix capture on all four backends, timestamped on the same clock as video, no `cpal` detour and no shelling out to `pactl`.
- **A frame model that doesn't lie** - every frame carries width/height/stride, pixel format, color-space hint, a monotonic timestamp, and a per-stream sequence number. Dropped frames surface as explicit `Gap` events.
- **Inspectable backend selection** - `Auto` picks the right backend per platform (with fallbacks like DXGI→WGC), and `session.backend_info()` tells you exactly what you got and its caveats.
- **Honest about platform differences** - X11 is documented as polling, DXGI as change-driven, Wayland as portal-mediated. No pretending all platforms behave the same.
## Platform support
| | Video | System audio | Notes |
| ----------------- | ------------------------------ | ------------------- | ------------------------------ |
| **Linux** Wayland | ✅ portal + PipeWire streaming | ✅ PipeWire | restore tokens skip the dialog |
| **Linux** X11 | ✅ paced polling (GetImage) | ✅ PipeWire | XFixes cursor blend |
| **macOS** 12.3+ | ✅ ScreenCaptureKit | ✅ ScreenCaptureKit | display + window capture |
| **Windows** 10+ | ✅ DXGI + WGC | ✅ WASAPI loopback | DXGI→WGC auto-fallback |
Full matrix with per-backend limitations: [docs/platforms.md](https://github.com/Itz-Agasta/pinray/blob/main/docs/platforms.md).
## Quick start
```console
cargo add pinray
```
```rust,no_run
use std::time::Duration;
use pinray::{AudioCapture, CaptureEvent, CaptureSession, SourceId, VideoCaptureTarget};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Capture the primary display plus everything the system plays.
let mut session = CaptureSession::builder()
.video_target(VideoCaptureTarget::Display(SourceId::new("auto")))
.audio(AudioCapture::SystemMix)
.build()?;
println!("backend: {:?}", session.backend_info().kind);
session.start()?;
loop {
match session.next_event(Some(Duration::from_secs(5)))? {
CaptureEvent::Video(f) => println!("video {}x{} t={}ns", f.width, f.height, f.stream_time_ns),
CaptureEvent::Audio(f) => println!("audio {} Hz {}ch", f.sample_rate, f.channels),
CaptureEvent::Gap(g) => println!("gap: {:?}", g.reason),
CaptureEvent::End => break,
}
}
session.stop()?;
Ok(())
}
```
Pick specific displays/windows with `pinray::enumerate_sources()`, capture audio-only without any permission dialogs, force a backend with `BackendPreference` - see [Getting started](https://github.com/Itz-Agasta/pinray/blob/main/crates/pinray/docs/getting-started.md).
## Requirements
| Platform | Build | Runtime |
| -------- | ------------------------------- | ---------------------------------------- |
| Linux | `libpipewire-0.3-dev` + `clang` | PipeWire; XDG portal for Wayland video |
| macOS | - | macOS 12.3+, Screen Recording permission |
| Windows | - | Windows 10+ (WGC needs 1903+) |
## Examples
```console
cargo run --example wayland_smoke # Linux Wayland: video + audio
cargo run --example x11_smoke # Linux X11: polling video + audio
cargo run --example audio_smoke # Linux/Windows: audio only
cargo run --example macos_smoke # macOS
cargo run --example windows_smoke # Windows (PINRAY_BACKEND=wgc|dxgi to force)
```
## Architecture
Cargo workspace: [`pinray-core`](https://github.com/Itz-Agasta/pinray/tree/main/crates/core) defines the frame model and backend traits, one crate per platform owns its native/unsafe code ([`platform-linux`](https://github.com/Itz-Agasta/pinray/tree/main/crates/platform-linux), [`platform-macos`](https://github.com/Itz-Agasta/pinray/tree/main/crates/platform-macos), [`platform-windows`](https://github.com/Itz-Agasta/pinray/tree/main/crates/platform-windows)), and [`pinray`](https://github.com/Itz-Agasta/pinray/tree/main/crates/pinray) is the public facade. A future encoder crate can slot in without touching capture.
## Stability
pinray is `0.2` and moving fast: minor versions may break the API while the frame model and builder settle against real-world use. Pin a minor version if you need stability.
## Minimum supported Rust version
Rust **1.88** (let-chains). MSRV bumps are minor-version changes during 0.2.
## Contributing
Issues and PRs welcome. Please include `session.backend_info()` output and `RUST_LOG=debug` logs in bug reports - backend selection differs per machine. CI runs clippy (warnings deny), tests on Linux/macOS/Windows, cross-target checks, and a real Xvfb capture smoke test.
## License
MIT - see [LICENSE](https://github.com/Itz-Agasta/pinray/blob/main/LICENSE). Contact: rupamgolui@proton.me