feat(screen-capture): native, AWT-free screen and window capture - #730
Draft
kdroidFilter wants to merge 1 commit into
Draft
kdroidFilter wants to merge 1 commit into
kdroidFilter wants to merge 1 commit into
Conversation
New screen-capture module: ScreenCapture.displays() / captureDisplay(display, region, includeCursor) / captureWindow(id) / permission, images in physical pixels with PNG and BGRA export. - Windows: GDI BitBlt in a per-monitor-v2 thread DPI context; windows through PrintWindow(PW_RENDERFULLCONTENT) on a worker thread with a timeout, since it waits forever on a thread that does not pump (a hung window, or the app's own UI thread blocked on the capture), falling back to the visible part on screen. - macOS: ScreenCaptureKit on 14+, CGDisplayCreateImageForRect via dlsym before. - Linux: X11 (XGetImage, RandR, XFixes cursor, XComposite), all dlopen'ed with X errors trapped per call; on Wayland the xdg-desktop-portal Screenshot interface. E2E: examples/screen-capture-demo self-test (pixel-exact pattern, occluded, minimized, blocked UI thread, hung foreign window, cursor, multi-thread torture) driven by scripts/screen-capture-windows-e2e.ps1 with GDI/handle leak sampling, and scripts/screen-capture-linux-e2e.sh (Xvfb at depths 24/16/8 against xwd, fake portal).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #637.
Summary
nucleus.screen-capture: native, AWT-free screen and window capture.java.awt.Robotreturns.BitBlt(CAPTUREBLT) in a per-monitor-v2 thread DPI context. Windows are captured withPrintWindow(PW_RENDERFULLCONTENT), which includes covered parts and DirectX/ANGLE content.SCScreenshotManager). Before 14 it usesCGDisplayCreateImageForRect/CGWindowListCreateImagethroughdlsym, because both are obsoleted in the 15 SDK. ScreenCaptureKit is weak-linked. The code waits on a semaphore with a timeout and never pumps the main run loop.XGetImage(any TrueColor mask, 16-bit, and 8-bit PseudoColor), RandR 1.5 monitors, the XFixes cursor, and XComposite for covered windows. Every library isdlopened and X errors are trapped per call, so a bad XID or a dying X server never takes the JVM down.org.freedesktop.portal.Screenshotover a private D-Bus connection. It captures the whole desktop, which is then cropped. The portal's file is decoded with gdk-pixbuf and then deleted.PrintWindowtrap is handled by design:PrintWindowsendsWM_PRINTand waits forever on a thread that does not pump messages. That covers a busy or hung window, and the app's own UI thread when it is blocked waiting for the capture. We verified that it blocks even whenIsHungAppWindowis already TRUE.examples/screen-capture-demo: an interactive demo (list displays, capture a display, a region or this window, preview, save PNG). It is also the E2E fixture (SCREEN_CAPTURE_DEMO_SELFTEST=1).build-natives.yaml,pre-merge.yamlandpublish-maven.yaml. The Linux apt line now nameslibxrandr-dev libxfixes-dev libxcomposite-devexplicitly; only their headers are needed, nothing is linked.Documentation
Installation
dependencies { implementation("dev.nucleusframework:nucleus.screen-capture:<version>") }Capturing a display
ScreenCapture.displays()lists the connected displays, primary first. EachCaptureDisplayhas:id\\.\DISPLAY1) on Windows, theCGDirectDisplayIDon macOS, the RandR output name on X11,portalon Waylandnameboundsnullon WaylandwidthPx/heightPx0on Wayland until the first capture)scaleFactordpi / 96) on Windows, px per point on macOS,1on X11 and WaylandisPrimaryCapturing a region
The region is in the display's own pixels, relative to its top-left corner: the coordinates of the image a full capture returns. It means the same thing on every platform.
The region is clipped to the display. A region that does not intersect the display throws
CaptureFailure.InvalidRegion.Capturing a window
What
windowIdis on each platform:windowIdHWNDTaoWindow.nativeHandleCGWindowID(NSWindow.windowNumber)TaoWindow.x11WindowIdCaptureFailure.WindowNotFound.isWindowCaptureSupportedisfalse, andcaptureWindowthrowsCaptureFailure.Unsupported.Using the pixels
ScreenImageholds opaque0xAARRGGBBpixels, row-major:Permissions
requestPermission()shows the system prompt once per app. A grant takes effect after the app restarts.NotDetermined.CaptureFailure.PermissionDenied.NotRequired.PermissionDenied, and dismissing the dialog throwsCancelled.Backends
ScreenCapture.backendis one ofGdi,ScreenCaptureKit,CoreGraphics,X11,XdgDesktopPortalorUnavailable.On Linux, the portal is used on a Wayland session (
XDG_SESSION_TYPE=wayland, orWAYLAND_DISPLAYset). XWayland's root window does not contain native Wayland windows, so X11 capture would miss them. To force a backend, pass-Dnucleus.screencapture.linuxBackend=x11or=portal.Errors
Every failure is a
ScreenCaptureException. Itsfailureis one of:UnsupportedPermissionDeniedCancelledDisplayNotFound(for example, the monitor was unplugged)WindowNotFoundInvalidRegionTimeoutFailedThe message includes the platform's diagnostic (a Win32 error, an
NSError, or a D-Bus error).Threading
Every function is safe to call from several threads at once. Every function blocks, so call them from
Dispatchers.IO.Capturing from the UI thread works, including capturing the app's own window, which is then printed inline. Avoid it anyway: the portal and ScreenCaptureKit can take a while to answer.
Test plan
:screen-capture:checkon Windows: unit and live tests, detekt, ktlint andapiCheck.Windows E2E,
scripts/screen-capture-windows-e2e.ps1 -Torture 1500: 39/39 checks, run twice.The test pattern is a 1-px checkerboard, a 0–255 ramp and a colour grid.
Pixel-exact captures of it:
A minimized window throws
WindowNotFound.A genuinely hung foreign window is captured in about 10 ms.
The cursor is drawn at its hotspot.
Display ids, bounds, scale and primary flag match
TaoMonitors.A 2560×1080 capture takes about 22 ms.
Torture: 9,516 captures from 11 threads, with random, extreme and
Int.MIN/Int.MAXregions, garbage HWNDs and concurrent window captures. Resources before and after:Linux E2E,
scripts/screen-capture-linux-e2e.sh(WSL, Xvfb): 14/14 scenarios.xwdat depths 24, 16 and 8, at 4K, and with RandR monitors.handle_token;Real Kotlin API on Linux (Xvfb, depth 24 with monitors and depth 16):
ScreenCaptureLiveTest+ScreenImageTest: 12/12.Cancelled,UnsupportedandPermissionDenied, and that the portal refuses window capture.macOS: never compiled locally.
build-nativesjob is the first build ofNucleusScreenCapture.m, with-Werror=unguarded-availability-new../gradlew :screen-capture:testand the demo.Windows HiDPI, and multiple monitors with mixed DPI: the test machine has a single display at 100 %.
A real Wayland portal (GNOME / KDE): only tested against the fake portal.
Linux aarch64 build (CI).