Repository navigation
Releases
v0.2.9
A small one: Windows audio behaves properly when you fall behind, and pinray is easier to find on crates.io.
Big thanks to @AndreIsraelAO for the Windows fix, their first contribution to pinray. They picked up #36, rewrote the WASAPI queue, and went through a full round of review including a drop guard so a panicking capture thread can't leave next_audio hanging forever.
Fixed
- Windows system audio drops the oldest packets, not the newest (#38, fixes #36). If your loop stalled for a moment, WASAPI kept the stale packets and threw away the fresh ones. Tested on a real Windows machine: after a ~2 s stall,
mainhanded back packets 1-64 and then jumped to 200, so you'd hear old audio right after catching up. Now you get 137-200 and then 201, the most recent audio. That's the same behavior as Linux, andsequencejumps still show you exactly how much was dropped.
Changed
- Better crates.io listing (#40). The description now mentions Wayland and screen recording, the keywords match what pinray actually does (
screenshotcomes back when there's a screenshot API), and the crates are listed in the specific video and audio categories, with each platform crate under its own OS API category, instead of the generic ones.
No API changes, so cargo update is all you need.
v0.2.8
Wayland can now list your displays, and docs.rs finally matches what pinray actually does.
On a Wayland session, enumerate_sources() used to return only the system-audio source. Thanks to @prushton2, it now lists every monitor straight from the compositor, with fractional scale where xdg-output is available, and no portal dialog (#26). Tested on COSMIC and KDE Plasma. The ids are informational, since the ScreenCast portal still decides which output gets captured.
The rest is docs. Every public type, method, and field now has a doc comment, with examples on the main entry points and an # Errors section on anything that returns Result. While writing them I checked each claim against all the backends, which turned up a few things the old docs got wrong or never said:
- X11's default frame rate is 60 like everywhere else, not 30. 30 is only what you get with
frame_rate(None). - Audio queues drop the oldest packets on Linux, but the newest on Windows and macOS.
- On macOS,
next_audio()returnsBackendNotSelected. Audio comes throughnext_eventthere (#34). - Wayland ignores
crop_rect, and no backend uses the builder'scolor_spaceyet (#30, #32).
Those are now documented, and the ones that are actually bugs have issues with enough detail for anyone to pick them up. Several are labeled good first issue.
Added
- Native Wayland display enumeration through
wl_output, no portal dialog (#26, by @prushton2)
Documentation
- Doc comments on the whole public API, with
missing_docsenabled so it stays that way (#28, closes #6) - Rewrote the README, getting-started guide, platform matrix, troubleshooting page, and
llms.txt; the README examples now compile as doctests (#35) - Added
CONTRIBUTING.md,SECURITY.md(private vulnerability reporting is on), and a bug report form (#35)
Changed
- More specific crates.io descriptions:
pinraycovers audio and names the native backends, and each sub-crate describes only what it contains
v0.2.7
A small macOS release, but a nice one: capture frames finally come out at the right size. Window capture used to give you a display-sized frame with your window squished somewhere inside it, and display capture assumed every screen is Retina. Both are fixed now.
Big thanks to @MohammediYunus for this one. I don't have a Mac, so he did the real work here: tracked down both bugs, fixed them, and tested everything on actual hardware with before/after recordings and a benchmark. Really appreciate it 🙏
Fixed
- Window capture is sized from the window (#19, fixes #15). Before, a 640x360 window came back as a full 3024x1964 display frame with the window letterboxed inside. Now you get the window at its own pixel size.
- Display capture uses the display's real scale (#21, fixes #20). Pinray assumed 2x everywhere, so a regular 1080p external monitor came out as 3840x2160. It now comes out at 1920x1080, and Retina displays are unchanged. That's 75% less pixel data per frame on 1x screens, and in his benchmark the capture process used about 64% less memory and 67% less CPU.
Behavior change
- macOS 13.0 is now the minimum (#18, closes #16). The stream setup uses audio settings that only exist on 13.0+, so 12.x was most likely crashing anyway. On older versions pinray now says so cleanly:
available_backends()is empty andtry_resolve()returnsBackendUnavailable.
Verified on
macOS 26.7.1 on an M5 Pro, with a built-in 2x display and an external 1x display. The recordings are on #19 and #21. Not tested yet: macOS 13, scaled "looks like" modes and rotated displays.
Still open
Crop rect sizing (#22), enumerate_sources placeholders (#23), audio channel layout (#24) and live window resize (#25). If you have a Mac and want to help, these are all up for grabs.
v0.2.6
Wayland frame_rate and queue_depth now actually do something, and dropped frames show up as gaps in sequence on every backend.
Fixed
- Wayland
frame_ratereaches the portal (#12, from #11). Screencast portals offer a variableframerate(0/1) and pace bymaxFramerate, but pinray only cappedframerate, so the limit never applied. It now goes intomaxFramerate.frame_rate(None)is uncapped and lands on the portal's own maximum, instead of silently becoming 60. - Wayland honors
queue_depth(#12). The event channel was unbounded, soqueue_depthdid nothing and memory could grow without limit behind a slow consumer. It is now bounded and drops the newest frame when full, like the other backends. - Wayland frames are no longer batched (#12). Frames sat in a queue that was only drained every 20 ms, adding up to 20 ms of latency and delivering them in bursts. They are now sent straight from the PipeWire callback.
- macOS frame drops show up in
sequence(#14, tracked in #13). The video and audio counters only advanced when a frame made it into thequeue_depthchannel, so frames dropped on a full queue left no gap and callers could not tell anything was lost. They now advance for every extracted frame, matching the other backends.
Behavior change
On Wayland, the default frame_rate: Some(60) is now passed to the portal as a maximum. Portals that honor maxFramerate will cap a faster display at 60 fps where earlier versions ran at whatever the portal produced. Pass .frame_rate(None) for uncapped. How strictly the cap is enforced is up to the portal: xdg-desktop-portal-hyprland, for example, overshoots it (about 40 fps delivered for a 30 fps cap).
Wayland capture rate itself is set by the compositor's portal, which only produces a frame when the screen content changes. Capture below the refresh rate on a busy screen is usually the portal, not pinray. See #11 for the measurements.
Also
VideoFrame::sequenceand the getting-started guide now describe the counter correctly: it advances per captured frame, including frames dropped on a full queue, and frames skipped byframe_ratepacing are not counted.docs/platforms.mdexplains how the Wayland frame cadence works.
Verified on
Arch Linux, Hyprland 0.56, xdg-desktop-portal-hyprland 1.4.1, PipeWire 1.6.9, 1920x1080 @ 60 Hz, Intel iGPU. frame_rate(None) negotiates the portal's max (120) with throughput unchanged, frame_rate(Some(30)) negotiates 30, and queue_depth 1 and 4 buffer exactly 1 and 4 frames behind a stalled consumer. GNOME and KDE portals were not tested. The macOS fix is compile and CI verified only, not yet run on a Mac.
v0.2.5
frame_rate is now honored on Windows. Both backends previously ignored it, so a session asking for 30 fps was handed frames at whatever rate the display or desktop produced.
Added
CaptureSession::restore_token()surfaces the Wayland portal restore token to callers, so a later session can skip the permission dialog. This landed after the 0.2.4 release and ships here.
Fixed
- WGC honors
frame_rate(#8, reported in #7). The free-threaded frame pool delivered a frame on every DWM composition, and frames overflowing the bounded channel were discarded only aftertexture_to_hosthad already run, so the GPU to CPU staging copy was paid for frames no consumer ever saw. Pacing now happens in the arrival callback before the copy. Measured at 1920x1080 @ 60 Hz with an encoder consuming at 30 fps: wasted copies went from 309 to 8 over ten seconds, and CPU from 29% of a core to 15%. - DXGI honors
frame_rate(#10, tracked in #9). Duplication is pull model, so it never wasted work, but a caller who set.frame_rate(Some(30))and looped onnext_eventgot frames as fast as the desktop changed. On a 144 Hz panel that is 144 fps of full frame copies into your encoder. The acquire is now held off until the frame is due, so a skipped frame is never copied. Unthrottled to 30 fps at 1080p, that is 59.9 fps and 32% of a core down to 29.6 fps and 11%. MinUpdateIntervalis set on WGC where the OS supports it, so the compositor throttles at the source. Note this lives onIGraphicsCaptureSession5and is Windows 11 only; Windows 10 returnsE_NOINTERFACEand the software pacing carries the rate limit there.
Behavior change
On a display faster than 60 Hz, the default frame_rate: Some(60) now caps delivery at 60 fps where earlier versions ran at full refresh. This is what the setting has always meant on the Linux and macOS backends, and Windows was the odd one out. Pass .frame_rate(None) to keep the old behavior and stream every frame the display or desktop produces.
Pacing is a ceiling, not a floor. A desktop changing more slowly than the requested rate still delivers at its own rate.
Two details worth knowing if you care about exact spacing:
- WGC can only select whole vblanks, so where the refresh rate is not an integer multiple of the request (60 fps on a 90 Hz panel) the gap between delivered frames alternates while the average holds.
- Frames skipped by pacing are not reported as
GaporDropped. You asked for that rate.
Also
chore: adoptedas_chunks_mutacross the platform crates forclippy::chunks_exact_to_as_chunks, new in Rust 1.98. CI pins stable with-D warnings, so this was breaking every job regardless of source changes.docs/platforms.mdnow records what each Windows backend does withframe_rate, and the feature matrix is corrected.
Verified on
Windows 10 19045, 1920x1080 @ 60 Hz, Intel UHD Graphics. Both backends deliver within 1% of the requested rate at 60, 30, 15 and 5 fps, with CPU and staging bandwidth scaling linearly. The MinUpdateInterval success path is compile verified only, since it needs a Windows 11 build that exposes IGraphicsCaptureSession5.
pinray 0.2.4 — Minor docs and crates.io tweaks
pinray 0.2.2 — Stride fix, reachable docs, agent-friendly reference
Small follow-up to 0.2.0 - no breaking changes, all additive.
Highlights
- Fixed a real silent-wrong bug:
VideoFrame.stridecan exceed the tightly-packed row size, and copyingFrameData::Hostbytes directly into arawvideopipe or encoder skewed every row after the first. AddedVideoFrame::to_tight_bytes()andPixelFormat::bytes_per_pixel()to fix it at the API level. - The getting-started guide is now the docs.rs front page (
include_str!'d into crate-level docs), instead of living only indocs/where docs.rs visitors never saw it. - Documented the frame-pacing footgun (declared constant fps vs push-driven actual capture cadence desyncs muxed audio/video) with a worked example.
- Audio-only capture now has its own first-class example instead of a one-line mention, plus an explicit callout that
AudioCapture::Microphoneisn't implemented on any backend yet (.build()fails eagerly withPinrayError::Unsupported). - Clarified why the docs.rs page redirects to a fixed target (no libpipewire on their Linux builders) and linked
docs.rs/pinray-coreas the always-correct, target-independent type reference. - Added
llms.txtat the repo root - a reference checked directly againstcrates/core/src/*.rs, for agents/tools integrating the crate.
Upgrading
Drop-in - no API removed or changed, only additions.
pinray 0.2.0 — Native Screen & System Audio Capture Across Linux, macOS, and Windows
Pinray 0.2.0 is the first feature-complete release, bringing native video capture and system audio support across Linux, macOS, and Windows through a unified Rust API.
Highlights
- Native display and window capture across all desktop platforms.
- System audio capture on Linux, macOS, and Windows.
- Consistent timestamps for audio/video synchronization.
- Improved documentation, examples, and CI.
Platform Support
Windows
- DXGI Desktop Duplication backend for high-performance display capture.
- Windows Graphics Capture (WGC) backend for window capture with automatic fallback.
- WASAPI shared-mode loopback for system audio capture.
- Shared boot-relative timestamps across audio and video.
macOS
- ScreenCaptureKit backend for display and window capture.
- System audio capture through
SCStream. - Improved screen-recording permission handling.
- Boot-relative timestamps with optional BGRA → RGBA conversion.
Linux
- Native X11 backend for display and window capture.
- PipeWire system audio capture.
- Shared monotonic timestamps for synchronized audio and video.
Core Improvements
- Added new backend kinds:
WindowsDxgiWindowsWgcWindowsWasapiLinuxX11LinuxPipeWireAudioMacosSckit
- Improved session muxing to prevent audio starvation.
Some(Duration::ZERO)is now guaranteed to perform a non-blocking poll.
Documentation & CI
- New getting-started guide and platform support matrix.
- Expanded rustdoc with runnable examples.
- CI now runs on Linux, macOS, and Windows.
- Fixed docs.rs builds.
⚠️ Breaking Changes
BackendKindandBackendPreferencegained new variants.- Timestamp semantics are now explicitly defined per platform. Consumers should compare timestamp deltas rather than absolute values across machines.
Thanks to everyone who tested Pinray during the 0.1.x releases! Feedback, bug reports, and contributions are always welcome.






