ScreenshotNeo

BlogEngineering

Why Android WebView Screenshots of Videos Are Blank and How to Fix Them

A WebView canvas snapshot can omit HTML5 video. Learn when to use hardware rendering, PixelCopy, or VirtualDisplay with SurfaceTexture.

By the ScreenshotNeo team30 September 20267 min read

Why Android WebView Screenshots of Videos Are Blank and How to Fix Them

Short answer: an HTML5 video can be rendered through a dedicated SurfaceView or another compositor surface outside the ordinary WebView canvas. A bitmap made with webView.draw(canvas), onDraw(), or a software layer may therefore contain the page but leave the video blank. Keep hardware acceleration enabled. Use PixelCopy for an accessible SurfaceView, or use Chromium WebView’s VirtualDisplay plus SurfaceTexture architecture for a snapshot of the whole WebView.

A blank result can also be intentional: FLAG_SECURE, a secure SurfaceView, or DRM-protected buffers are designed to disappear from screenshots. No rendering tweak should bypass that protection.

Why the video is missing

Android WebView renders HTML, CSS, and video through Chromium’s compositor. Ordinary canvas drawing asks the view to paint its software-visible contents. Video frames may instead be queued into a separate surface, so the canvas bitmap contains the surrounding DOM but not the video surface.

Chromium documents that software drawing has missing features such as <video> and <webgl>. See the Chromium WebView guidance. Android’s PixelCopy API copies from a Surface or SurfaceView, rather than arbitrary DOM pixels.

Diagnose the capture path

  1. Find the snapshot call. If it invokes webView.draw(), webView.onDraw(), or a software bitmap cache, expect video omission. Chromium specifically warns against using those calls for this snapshot use case.
  2. Check acceleration. Log webView.isHardwareAccelerated() and the canvas acceleration state. Remove setLayerType(View.LAYER_TYPE_SOFTWARE, ...) from the WebView and its parents. Do not disable application or window hardware acceleration.
  3. Identify the source surface. If the player exposes a SurfaceView, capture that surface with PixelCopy after playback has displayed a frame.
  4. Check security flags. Look for WindowManager.LayoutParams.FLAG_SECURE, SurfaceView.setSecure(true), and DRM-protected output. A blank screenshot is expected for protected content.
  5. Choose the architecture. Use PixelCopy for a known SurfaceView. Use VirtualDisplay and SurfaceTexture when the snapshot must include the complete WebView compositor output.
A WebView canvas and a video surface are separate capture sources.
A WebView canvas and a video surface are separate capture sources.

Keep hardware acceleration enabled

Hardware acceleration is enabled by default for applications targeting API 14 and later. Do not add android:hardwareAccelerated="false", and do not put the WebView or a parent view on a software layer.

fun logAcceleration(webView: WebView, canvas: Canvas) {
    Log.d("Capture", "view=${webView.isHardwareAccelerated} canvas=${canvas.isHardwareAccelerated}")
}

A software layer can help ordinary custom drawing, but it is unsuitable for pages containing video because Chromium’s software path does not provide complete video support.

Capture a video SurfaceView with PixelCopy

PixelCopy.request(SurfaceView, ...) is available from API 24. Allocate a bitmap at the surface’s displayed size, wait until the surface is valid and has queued a frame, and treat only SUCCESS as success.

@RequiresApi(Build.VERSION_CODES.N)
fun copySurfaceView(
    surfaceView: SurfaceView,
    onResult: (Bitmap?, Int) -> Unit
) {
    if (!surfaceView.holder.surface.isValid || surfaceView.width == 0 || surfaceView.height == 0) {
        onResult(null, PixelCopy.ERROR_SOURCE_INVALID)
        return
    }

    val bitmap = Bitmap.createBitmap(
        surfaceView.width,
        surfaceView.height,
        Bitmap.Config.ARGB_8888
    )

    PixelCopy.request(
        surfaceView,
        bitmap,
        { result ->
            if (result == PixelCopy.SUCCESS) {
                onResult(bitmap, result)
            } else {
                bitmap.recycle()
                onResult(null, result)
            }
        },
        Handler(Looper.getMainLooper())
    )
}

Call it after the player reports that playback has started and after the surface has had time to queue a frame. A retry loop should be bounded and delayed, rather than issuing requests in a tight loop.

Result Meaning Fix
SUCCESS The bitmap contains the copied surface. Save or process the bitmap.
ERROR_SOURCE_NO_DATA No frame has been queued yet. Wait for playback or a rendered-frame callback, then retry.
ERROR_SOURCE_INVALID The surface was destroyed, detached, or otherwise invalid. Wait for a new valid surface and retry.
ERROR_TIMEOUT The copy did not finish in time. Retry later and investigate device load.
ERROR_DESTINATION_INVALID The destination bitmap cannot receive the copy. Use a mutable, correctly sized bitmap.

PixelCopy copies the surface rectangle. It does not reconstruct arbitrary HTML elements around that surface. If you need the page and video in one image, use the WebView compositor approach below or combine separately captured regions when your layout permits it.

Capture the complete WebView with VirtualDisplay and SurfaceTexture

For a WebView-wide image that includes compositor surfaces, Chromium’s documented architecture is a VirtualDisplay backed by a SurfaceTexture. The WebView is attached to a Presentation on that display; the SurfaceTexture receives the compositor output as a GL texture, which is then read back into a bitmap.

VirtualDisplay and SurfaceTexture preserve the compositor output for a WebView-wide snapshot.
VirtualDisplay and SurfaceTexture preserve the compositor output for a WebView-wide snapshot.
  1. Create an external OES texture and a SurfaceTexture with the target width and height.
  2. Wrap the texture in a Surface and create a VirtualDisplay with DisplayManager.createVirtualDisplay().
  3. Create a Presentation for the virtual display, put the WebView in its content view, and show it.
  4. Wait for the page to load and for video playback to produce a frame.
  5. On the GL thread, call surfaceTexture.updateTexImage(), draw the texture into an FBO, and read pixels with glReadPixels() into a bitmap.
  6. Release the Presentation, VirtualDisplay, Surface, SurfaceTexture, and GL resources in a finally block.
// Architecture outline; run display and GL work on dedicated threads.
val texture = SurfaceTexture(oesTextureId)
texture.setDefaultBufferSize(width, height)
val outputSurface = Surface(texture)
val display = displayManager.createVirtualDisplay(
    "WebViewCapture", width, height, densityDpi,
    outputSurface, DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
)
val presentation = Presentation(context, display.display)
presentation.setContentView(webView)
presentation.show()

// After page load and a rendered video frame on the GL thread:
texture.updateTexImage()
// Bind an FBO, draw the external texture, then glReadPixels(...)
// into a correctly sized ARGB/RGBA bitmap.

presentation.dismiss()
display.release()
outputSurface.release()
texture.release()

The exact EGL shader, FBO setup, and lifecycle code depend on your renderer. Keep all SurfaceTexture operations on the thread that owns its GL context, resize the buffer when the capture dimensions change, and do not destroy the virtual display while a frame is being read.

Protected content cannot be captured

FLAG_SECURE prevents screenshots and non-secure-display output; Android documents that “when the screenshot is taken the resulting image is blank.” A SurfaceView made secure with setSecure(true) has the same intent. DRM-protected buffers may also be invisible when secure output or protected-buffer support is unavailable. Explain the restriction or use an authorized non-protected stream. Do not attempt to defeat it.

Common failures and fixes

Symptom Likely cause Action
Page is visible but video is a blank rectangle Canvas or software capture omitted the video surface. Keep hardware acceleration on; use PixelCopy or VirtualDisplay/SurfaceTexture.
PixelCopy returns ERROR_SOURCE_NO_DATA Playback has not queued a frame. Wait for a rendered frame and retry with a timeout.
PixelCopy returns ERROR_SOURCE_INVALID Surface lifecycle changed. Reacquire the current SurfaceView and check isValid.
Everything is blank, including HTML Secure window, secure surface, DRM, or an unloaded page. Inspect security flags and page load state; protected output is intentionally blank.
Works on one device but not another Differences in Android API level, WebView package, GPU driver, or DRM. Test the supported path across the target API and device matrix.
VirtualDisplay image is stale No buffer update was consumed. Use OnFrameAvailableListener, call updateTexImage() on the GL thread, then read back.

Performance, reliability, and cost

  • PixelCopy: one surface-to-bitmap copy is usually simpler and cheaper than a full GL pipeline, but it depends on a valid surface and a queued frame.
  • VirtualDisplay: uses display, surface, and GL resources; reuse the renderer for batches, cap bitmap dimensions, and release every resource promptly.
  • Timing: page load does not guarantee a video frame. Wait for playback state or frame availability.
  • Memory: an ARGB bitmap uses about four bytes per pixel, before intermediate textures and FBOs. Avoid several full-resolution copies at once.
  • Reliability: WebView, Android framework, GPU drivers, and DRM behavior vary. Validate on the API levels and devices you support.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API when you do not want to manage WebView surfaces, virtual displays, EGL, or device-specific behavior. The API accepts one GET request and returns PNG, JPEG, WebP, or PDF. Cookie banners, newsletter popups, and chat widgets are removed 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 status. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Free accounts include 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Can I fix this by disabling hardware acceleration?

No. That makes video omission more likely because Chromium’s software drawing path has missing video support.

Does PixelCopy capture any HTML video element?

Only when you can access the actual video SurfaceView or Surface. PixelCopy does not copy arbitrary DOM pixels.

Why is PixelCopy black instead of returning an error?

The source may be protected, secure, not yet composited, or producing a frame the device cannot expose. Check result codes, security flags, and frame timing.

Which method captures the entire WebView?

Use the VirtualDisplay plus SurfaceTexture compositor path, then read the texture into a bitmap.

Can DRM video be captured for debugging?

Not when the stream or surface requires secure output. Use an authorized non-protected test stream or inspect playback telemetry instead.