Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Take Screenshot of Surface View Shows Black Screen

People also ask

Why does my screen go black when I take a screenshot?

This can be caused by apps that turn the screen black to prevent capturing such as video streaming apps, games, overlays, or anti-virus software. It may also be caused by using Windows without activating it.

How do you force a screenshot on an Android?

To take a screenshot on Android, press and hold the Power button then choose Screenshot from the menu. If there is no screenshot restriction imposed by the app, the image saves to Device > Pictures > Screenshots by default. However, if you see a notification that says, “Couldn't save screenshot.


There is a great deal of confusion about this, and a few correct answers.

Here's the deal:

  1. A SurfaceView has two parts, the Surface and the View. The Surface is on a completely separate layer from all of the View UI elements. The getDrawingCache() approach works on the View layer only, so it doesn't capture anything on the Surface.

  2. The buffer queue has a producer-consumer API, and it can have only one producer. Canvas is one producer, GLES is another. You can't draw with Canvas and read pixels with GLES. (Technically, you could if the Canvas were using GLES and the correct EGL context was current when you went to read the pixels, but that's not guaranteed. Canvas rendering to a Surface is not accelerated in any released version of Android, so right now there's no hope of it working.)

  3. (Not relevant for your case, but I'll mention it for completeness:) A Surface is not a frame buffer, it is a queue of buffers. When you submit a buffer with GLES, it is gone, and you can no longer read from it. So if you were rendering with GLES and capturing with GLES, you would need to read the pixels back before calling eglSwapBuffers().

With Canvas rendering, the easiest way to "capture" the Surface contents is to simply draw it twice. Create a screen-sized Bitmap, create a Canvas from the Bitmap, and pass it to your draw() function.

With GLES rendering, you can use glReadPixels() before the buffer swap to grab the pixels. There's a (less-expensive than the code in the question) implementation of the grab code in Grafika; see saveFrame() in EglSurfaceBase.

If you were sending video directly to a Surface (via MediaPlayer) there would be no way to capture the frames, because your app never has access to them -- they go directly from mediaserver to the compositor (SurfaceFlinger). You can, however, route the incoming frames through a SurfaceTexture, and render them twice from your app, once for display and once for capture. See this question for more info.

One alternative is to replace the SurfaceView with a TextureView, which can be drawn on like any other Surface. You can then use one of the getBitmap() calls to capture a frame. TextureView is less efficient than SurfaceView, so this is not recommended for all situations, but it's straightforward to do.

If you were hoping to get a composite screen shot containing both the Surface contents and the View UI contents, you will need to capture the Canvas as above, capture the View with the usual drawing cache trick, and then composite the two manually. Note this won't pick up the system parts (status bar, nav bar).

Update: on Lollipop and later (API 21+) you can use the MediaProjection class to capture the entire screen with a virtual display. There are some trade-offs with this approach, e.g. you're capturing the rendered screen, not the frame that was sent to the Surface, so what you get may have been up- or down-scaled to fit the window. In addition, this approach involves an Activity switch since you have to create an intent (by calling createScreenCaptureIntent on the ProjectionManager object) and wait for its result.

If you want to learn more about how all this stuff works, see the Android System-Level Graphics Architecture doc.


I know its a late reply but for those who face the same problem,

we can use PixelCopy for fetching the snapshot. It's available in API level 24 and above

PixelCopy.request(surfaceViewObject,BitmapDest,listener,new Handler());

where,

surfaceViewObject is the object of surface view

BitmapDest is the bitmap object where the image will be saved and it cant be null

listener is OnPixelCopyFinishedListener

for more info refer - https://developer.android.com/reference/android/view/PixelCopy


This is because SurfaceView uses OpenGL thread for drawing and draws directly to a hardware buffer. You have to use glReadPixels (and probably a GLWrapper).

See the thread: Android OpenGL Screenshot


Update 2020 view.setDrawingCacheEnabled(true) is deprecated in API 28

If you are using normal View then you can create a canvas with the specified bitmap to draw into. Then ask the view to draw over that canvas and return bitmap filled by Canvas

 /**
     * Copy View to Canvas and return bitMap
     */
    fun getBitmapFromView(view: View): Bitmap? {
        var bitmap =
            Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        view.draw(canvas)
        return bitmap
    }

Or you can fill the canvas with a default color before drawing it with the view:

    /**
     * Copy View to Canvas and return bitMap and fill it with default color
     */
    fun getBitmapFromView(view: View, defaultColor: Int): Bitmap? {
        var bitmap =
            Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        var canvas = Canvas(bitmap)
        canvas.drawColor(defaultColor)
        view.draw(canvas)
        return bitmap
    }

The above approach will not work for the surface view they will drawn as a hole in the screenshot.

For surface view since Android 24, you need to use Pixel Copy.

    /**
     * Pixel copy to copy SurfaceView/VideoView into BitMap
     */
     fun usePixelCopy(videoView: SurfaceView,  callback: (Bitmap?) -> Unit) {
        val bitmap: Bitmap = Bitmap.createBitmap(
            videoView.width,
            videoView.height,
            Bitmap.Config.ARGB_8888
        );
        try {
        // Create a handler thread to offload the processing of the image.
        val handlerThread = HandlerThread("PixelCopier");
        handlerThread.start();
        PixelCopy.request(
            videoView, bitmap,
            PixelCopy.OnPixelCopyFinishedListener { copyResult ->
                if (copyResult == PixelCopy.SUCCESS) {
                    callback(bitmap)
                }
                handlerThread.quitSafely();
            },
            Handler(handlerThread.looper)
        )
        } catch (e: IllegalArgumentException) {
            callback(null)
            // PixelCopy may throw IllegalArgumentException, make sure to handle it
            e.printStackTrace()
        }
    }

This approach can take a screenshot of any subclass of Surface Vie eg VideoView

Screenshot.usePixelCopy(videoView) { bitmap: Bitmap? ->
                processBitMap(bitmap)
            }

Here is the complete method to take screen shot of a surface view using PixelCopy. It requires API 24(Android N).

@RequiresApi(api = Build.VERSION_CODES.N)
private void capturePicture() {
    Bitmap bmp = Bitmap.createBitmap(surfaceView.getWidth(), surfaceView.getHeight(), Bitmap.Config.ARGB_8888);
    PixelCopy.request(surfaceView, bmp, i -> {
        imageView.setImageBitmap(bmp); //"iv_Result" is the image view
    }, new Handler(Looper.getMainLooper()));
}