ScreenshotNeo

BlogEngineering

How to Remove Extra Padding from Android MediaProjection Screenshots

Remove black bars from Android MediaProjection screenshots by matching the captured region, VirtualDisplay, and output Surface dimensions.

By the ScreenshotNeo team1 October 20268 min read

Extra padding in a MediaProjection screenshot is usually black letterboxing caused by a dimension or aspect-ratio mismatch. The captured region, VirtualDisplay, and output Surface must use the same width and height. For Android 14 (API 34) and later, update those dimensions from MediaProjection.Callback.onCapturedContentResize() whenever the selected app window or display changes size.

Android describes the symptom directly: when recorded content has a different aspect ratio from the virtual display or output surface, the captured stream gets black bars around the content. See the Media projection guide and the Android 14 screen-sharing explanation.

1. Why MediaProjection screenshots have extra padding

MediaProjection does not expose a separate “remove padding” switch. Padding is normally unused surface area introduced when the output dimensions do not match the captured content.

  • Full-display capture: the initial size should come from the device’s maximum window metrics.
  • App-window capture on Android 14+: the user may select one app window, whose bounds can differ from the physical display.
  • Rotation or window changes: the captured region can change after capture starts.
  • Android 12L and later scaling: Android preserves aspect ratio and centers content when the surface and content differ, which can leave unused space.

Do not solve a letterboxing problem by cropping the bitmap first. Correct the virtual-display and surface dimensions so the produced image has the intended bounds. Crop only when you intentionally want a smaller region than the captured content.

2. The dimension-matching fix

  1. Determine the expected capture bounds.
  2. Register a MediaProjection.Callback before creating the virtual display.
  3. Create the virtual display and output surface with identical width and height.
  4. On every API 34 resize callback, recreate or resize the output surface and call VirtualDisplay.resize(width, height, densityDpi).
  5. Release the virtual display and surface in onStop().

Choosing the initial bounds

For a full-display projection, Android recommends WindowManager.getMaximumWindowMetrics(). Its bounds represent the maximum display area even when the host app is in multi-window mode. On Android 11 (API 30) and newer, use the platform API. For older devices, Jetpack’s WindowMetricsCalculator.computeMaximumWindowMetrics() provides a compatibility path down to API 14.

For Android 14 app sharing, the initial display size is not guaranteed to be the selected app window size. Treat the resize callback as authoritative for the selected region.

3. Kotlin implementation

The following example shows the important lifecycle and resize path. It uses an ImageReader as the output surface. In a production app, connect its images to your encoder or bitmap pipeline and handle permissions and foreground-service requirements appropriate to your target SDK.

class ProjectionCapture(
    private val context: Context,
    private val projection: MediaProjection
) {
    private var virtualDisplay: VirtualDisplay? = null
    private var imageReader: ImageReader? = null
    private var width = 0
    private var height = 0
    private val densityDpi = context.resources.configuration.densityDpi

    private val callback = object : MediaProjection.Callback() {
        override fun onCapturedContentResize(newWidth: Int, newHeight: Int) {
            if (Build.VERSION.SDK_INT >= 34) {
                resizeCapture(newWidth, newHeight)
            }
        }

        override fun onStop() {
            virtualDisplay?.release()
            virtualDisplay = null
            imageReader?.close()
            imageReader = null
        }
    }

    fun start() {
        projection.registerCallback(callback, Handler(Looper.getMainLooper()))

        val metrics = if (Build.VERSION.SDK_INT >= 30) {
            (context.getSystemService(WindowManager::class.java))
                .maximumWindowMetrics
        } else {
            // Replace this branch with WindowMetricsCalculator from AndroidX.
            @Suppress("DEPRECATION")
            val dm = DisplayMetrics().also {
                (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
                    .defaultDisplay.getRealMetrics(it)
            }
            null
        }

        val bounds = metrics?.bounds
        val initialWidth = bounds?.width() ?: 1080
        val initialHeight = bounds?.height() ?: 1920
        resizeCapture(initialWidth, initialHeight)
    }

    private fun resizeCapture(newWidth: Int, newHeight: Int) {
        if (newWidth <= 0 || newHeight <= 0) return
        if (newWidth == width && newHeight == height && virtualDisplay != null) return

        width = newWidth
        height = newHeight

        // ImageReader surfaces have fixed dimensions. Recreate it for the new bounds.
        imageReader?.close()
        imageReader = ImageReader.newInstance(
            width,
            height,
            PixelFormat.RGBA_8888,
            2
        )

        val surface = imageReader!!.surface
        val display = virtualDisplay
        if (display == null) {
            virtualDisplay = projection.createVirtualDisplay(
                "ScreenshotCapture",
                width,
                height,
                densityDpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                surface,
                null,
                Handler(Looper.getMainLooper())
            )
        } else {
            display.resize(width, height, densityDpi)
            display.setSurface(surface)
        }
    }

    fun stop() {
        projection.unregisterCallback(callback)
        virtualDisplay?.release()
        virtualDisplay = null
        imageReader?.close()
        imageReader = null
    }
}

The fallback branch above is only for the initial size on older Android versions. Use AndroidX WindowMetricsCalculator when supporting API 14–29 rather than relying on deprecated display metrics in new code. The density passed to createVirtualDisplay() should come from Configuration.densityDpi; Android’s guide says Display.getRealMetrics() is deprecated for this purpose.

API 34 resize behavior

onCapturedContentResize(width, height) is available from API 34. Android invokes it when capture starts and whenever the selected content changes size. The callback dimensions correspond to the captured region’s bounds, including an app window selected through Android 14 screen sharing. Always update both outputs together.

Surface choices

Output When to use it Padding consideration
ImageReader Reading frames into bitmaps or image processing Recreate it when dimensions change because its buffer size is fixed.
Encoder surface Video or image encoding pipelines Configure the encoder to the same width and height as the captured region.
Application surface Displaying the projection in your UI Make the surface’s buffer size and aspect ratio match the projection.

4. Rotation, multi-window, and app-window capture

A capture can change size because the device rotates, the selected app enters split screen, a freeform window is resized, or the user changes the sharing target. Do not assume the initial full-display dimensions remain valid.

  • Keep the resize callback registered for the entire projection lifetime.
  • Debounce repeated callbacks if your surface recreation is expensive, but apply the newest dimensions.
  • Do not swap width and height based only on an orientation flag; use the callback’s actual values.
  • When the user selects an app window on Android 14, expect system UI such as status and navigation bars to be excluded from that app-window capture.

Android 12L introduced uniform scaling that preserves aspect ratio and centers content inside the surface. If you still see bars after implementing callbacks, log the captured width and height, the virtual-display width and height, and the surface buffer dimensions at the same moment. One of those values is usually stale.

5. Lifecycle rules and Android 14 limits

Register the callback before creating the virtual display. Release the virtual display and output surface in onStop(), including when the user revokes capture permission or the system ends the projection.

Android 14 limits a projection token to one virtual-display creation. A resize path must therefore resize the existing display and replace its surface; it should not create a second virtual display with the same token. See the MediaProjection API reference and the Android 14 SDK source.

6. Debugging checklist

  • Log the captured-content width and height from onCapturedContentResize().
  • Log the width, height, and density passed to createVirtualDisplay().
  • Log the output surface or image-reader dimensions.
  • Check whether the screenshot is from a full display or a selected app window.
  • Rotate the device and resize the app window while capturing.
  • Verify that you are not reusing an old surface after a resize.
  • Inspect the final bitmap dimensions before adding any crop operation.

7. Common errors and fixes

Symptom Likely cause Fix
Black bars on two sides Surface and content have different aspect ratios. Use the captured region’s width and height for both the virtual display and surface.
Bars appear only after rotation Initial dimensions are still being used. Handle every onCapturedContentResize() callback and resize both outputs.
App-window capture has display-sized padding The virtual display was created with physical-display dimensions. Wait for API 34 callback dimensions and apply the selected window bounds.
Resize callback never runs Running below API 34 or callback was registered too late. Guard the callback with Build.VERSION.SDK_INT >= 34 and register it before display creation; use initial metrics on older APIs.
Images are stretched Only one output was resized, or a consumer is forcing another aspect ratio. Resize the surface, virtual display, encoder, and image-processing target consistently.
SecurityException when creating another display The same Android 14 projection token was used to create a second virtual display. Reuse and resize the existing virtual display; do not create a replacement with that token.
Capture stops unexpectedly User revoked permission, the projection ended, or resources were released. Handle onStop(), close the surface, release the display, and request a new projection when appropriate.
Blank or incomplete frames The consumer reads before a valid frame is available or the surface was swapped during processing. Coordinate frame acquisition with your consumer and discard frames from a closed reader.

8. Performance and reliability

  • Resize work: recreating an ImageReader allocates buffers. Avoid unnecessary recreation when the callback repeats the current dimensions.
  • Memory: large full-display buffers can be expensive. Keep the reader’s buffer count modest and close images promptly.
  • Threading: perform surface and virtual-display changes on a controlled thread, while keeping frame processing off the main thread.
  • Rotation: treat each resize as a state transition and make the newest dimensions win if callbacks arrive quickly.
  • Cleanup: release the virtual display, surface, reader, and callback registration on every stop path.
  • Validation: test full-display and app-window sharing, portrait and landscape rotation, split screen, freeform resize, and projection cancellation.

9. Or skip the browser setup

If your goal is a clean screenshot of a web page rather than an Android device surface, ScreenshotNeo provides a single HTTP request that returns PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms along with newsletter popups and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. It also provides an MCP server so Claude, Cursor, and other MCP clients can call take_screenshot, get_page_info, and capture_pdf.

cURL (see the ScreenshotNeo API documentation):

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo includes full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work.

The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.

10. FAQ

Is the padding caused by MediaProjection itself?

Usually no. It is the visible result of captured content and output dimensions or aspect ratios not matching.

Should I crop the screenshot after capture?

Only for an intentional crop. For black bars, correct the virtual-display and surface sizes first.

Does onCapturedContentResize() work on every Android version?

No. It is available from API 34. Older versions need an appropriate initial metrics strategy and their own configuration-change handling.

Why can maximum display metrics be wrong for Android 14 app sharing?

Android 14 lets the user share a single app window. That selected window can be smaller or have a different aspect ratio than the display, so the callback dimensions are needed.

Can I create a second virtual display after a resize?

Do not do so with the same Android 14 projection token. Resize the existing virtual display and replace or resize its surface.