7

I am using Android ImageReader class to receive Bitmaps from MediaProjection.createVirtualDisplay method.

My code so far looks like this:

mProjection.createVirtualDisplay("test", width, height, density, flags, mImageReader.getSurface(), new VirtualDisplayCallback(), mHandler);
            mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
                @Override
                public void onImageAvailable(ImageReader reader) {
                    Image image = null;
                    try {
                        image = mImageReader.acquireLatestImage();
                        final Image.Plane[] planes = image.getPlanes();
                        final ByteBuffer buffer = planes[0].getBuffer();
                        final byte[] data = new byte[buffer.capacity()];
                        buffer.get(data);
                        final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                        if (bitmap==null)
                            Log.e(TAG, "bitmap is null");

                    } catch (Exception e) {
                        if (image!=null)
                            image.close();
                    }
                }

            }, mHandler);

The problem is that BitmapFactory cannot decode data[] back to Bitmap, i.e. BitmapFactory always returns null. The only messages I see from logcat come from android_media_ImageReader.cpp and go like this:

D/ImageReader_JNI(1432): ImageReader_imageSetup: Receiving JPEG in HAL_PIXEL_FORMAT_RGBA_8888 buffer.
W/ImageReader_JNI(1432): Image_getJpegSize: No JPEG header detected, defaulting to size=width=3891200

Image object returned by acquireLatestImage is not null but not a valid JPEG either, I tried to check with the following test which fails:

if((buf [0] & 0xFF) == 0xFF && (buf[1] & 0xFF) == 0xD8 && (buf[2] & 0xFF) == 0xFF && (buf[3] & 0xFF) == 0xE0)
    Log.e(TAG, "is JPG");
else
    Log.e(TAG, "not a valid JPG");

The only think I am suspecting at the moment is that Android 5.0 emulator I am testing against cannot hanlde the API calls.

Any ideas?

mtsahakis
  • 613
  • 1
  • 8
  • 16
  • There are a few good answers below which solve your problem. Please select one as accepted answer so other people can benefit. – binW Mar 13 '15 at 12:42

6 Answers6

13

The code in answer by @charlesjean works but I would rather not generate each pixel by my self. A better way to get the Image from ImageReader is just to create right sized bitmap and use the method copyPixelsFromBuffer(). Create ImageReader as follows:

mImageReader = ImageReader.newInstance(mWidth, mHeight, ImageFormat.RGB_565, 2);

Then you can get the image from mImageReader using the code below.

final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int offset = 0;
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * mWidth;
// create bitmap
bitmap = Bitmap.createBitmap(mWidth+rowPadding/pixelStride, mHeight, Bitmap.Config.RGB_565);
bitmap.copyPixelsFromBuffer(buffer);
image.close();

I have described the process of capturing screen using MediaProjection API along with the mistakes most people made when getting image from ImageReader in a blog post which you can read if interested.

binW
  • 12,660
  • 10
  • 54
  • 67
  • 4
    Thank you, this worked for me! Although I had to change "ImageFormat.RGB_565" to "PixelFormat.RGBA_8888" and "Bitmap.Config.RGB_565" to "Bitmap.Config.ARGB_8888" or else when I called "image = reader.acquireLatestImage();" it would throw an exception because of an incompatible format. – joaomgcd Mar 06 '15 at 11:30
  • @joaomgcd ARGB_8888 results in 4 bytes per pixels meaning bigger bitmaps compared to RGB_565 which takes 2 bytes per pixel. I needed smaller bitmaps but otherwise it is perfectly fine to use ARGB_8888 if you are not worried about memory. They crash must be for some other reason as this code is copied exactly from an app that I have developed and is working fine, no crashes there. – binW Mar 06 '15 at 14:12
  • I tested the code and I got `03-11 22:12:27.653 7990-8008/com.mtsahakis.mediaprojectiondemo E/ImageReader_JNI﹕ Producer output buffer format: 0x1, ImageReader configured format: 0x4 03-11 22:12:27.653 7990-8008/com.mtsahakis.mediaprojectiondemo W/System.err﹕ java.lang.UnsupportedOperationException: The producer output buffer format 0x1 doesn't match the ImageReader's configured buffer format 0x4. 03-11 22:12:27.653 7990-8008/com.mtsahakis.mediaprojectiondemo W/System.err﹕ at android.media.ImageReader.nativeImageSetup(Native Method)` when `acquireLatestImage()` is called. Any ideas? – mtsahakis Mar 11 '15 at 22:22
  • 2
    I also had to change from "ImageFormat.RGB_565" to "PixelFormat.RGBA_8888" and "Bitmap.Config.RGB_565" to "Bitmap.Config.ARGB_8888" to make it work. – mtsahakis Mar 12 '15 at 12:58
  • 1
    This appears to be very device dependent. The ImageReader formats that I have available are only those listed at https://developer.android.com/reference/android/media/Image.html, and I can only use ImageFormat.JPEG, which causes the plane to have a pixelStride of 0, so I'm getting a division by zero exception. – Nicolas Mar 12 '15 at 20:32
  • @Nicolas JPEG will will not work. I don't think ImageReader can give you any kind of compressed image. I have tried using JPEG and it throws an exception even if you work around DevisionByZero. I am using Nexus5 with latest SDK and RGB_565 works fine for me. You may have more luck with YUV_420_888 format but why can you only use formats from Image class? – binW Mar 13 '15 at 09:36
  • @binW This has been my experience on an HTC One M8. The ImageReader does no compression, rather the CameraDevice does.The only ImageReader formats I can use is NV21, JPEG, YUV_420_888 and YV12. I cannot use any other ImageFormat, not SENSOR_RAW, or anything else – Nicolas Mar 13 '15 at 12:25
  • @binW That worked for me but as joaomgcd mentioned I had to change certain parameters here and there. – Amit Gupta Aug 19 '15 at 15:20
  • I can't find any combination of parameters that works on a Samsung S6 - I get "java.lang.UnsupportedOperationException: The producer output buffer format 0x22 doesn't match the ImageReader's configured buffer format 0x4." (or 0x1 depending on what params I pass) - has anyone had any luck with this? Currently I can't find any solution that would work with all devices, to be able to release this code. – androidneil May 06 '16 at 21:26
  • @Nicolas To read JPEG better use `BitmapFactory` as in the question—both its docs and the errors in there clarify that `BitmapFactory` reads JPEGs into `Bitmap`. This question is about reading from `RGBA_8888` images. – Blaisorblade Sep 11 '16 at 11:12
  • For me this gives java.lang.ArithmeticException: divide by zero as plane.getPixelStride() returns 0 – Daniel Viglione Jul 08 '17 at 00:45
  • Unable to find PixelFormat enum in xamarin android ,how to set it ? – VINNUSAURUS Nov 11 '18 at 20:35
9

I encountered exactly your problem. My ImageReader created as so:

ImageReader.newInstance(mCaptureSize.getWidth(), mCaptureSize.getHeight(), ImageFormat.JPEG, 1);

The ImageReader above should only return compressed images, and these need to be decompressed. I acquireLatestImage(), then pass it through the following:

ByteBuffer bBuffer = planes[0].getBuffer;
bBuffer.rewind();
byte[] buffer = new byte[bBuffer.remaining()];
planes[0].getBuffer().get(buffer);
Bitmap bitmap = BitmapFactory.decodeByteArray(buffer, 0, buffer.length);

The key for me was to rewind the ByteBuffer. Your code should work as so:

mProjection.createVirtualDisplay("test", width, height, density, flags, mImageReader.getSurface(), new VirtualDisplayCallback(), mHandler);
            mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
                @Override
                public void onImageAvailable(ImageReader reader) {
                    Image image = null;
                    try {
                        image = mImageReader.acquireLatestImage();
                        final Image.Plane[] planes = image.getPlanes();
                        final ByteBuffer buffer = planes[0].getBuffer();
                        buffer.rewind()
                        final byte[] data = new byte[buffer.capacity()];
                        buffer.get(data);
                        final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                        if (bitmap==null)
                            Log.e(TAG, "bitmap is null");

                    } catch (Exception e) {
                        if (image!=null)
                            image.close();
                    }
                }

            }, mHandler);

I don't like having to copy the ByteBuffer through an intermediate byte[], but the internal array is protected.

Tested working on 5.0.1 on an HTC

Nicolas
  • 1,046
  • 11
  • 25
  • How come I have black margins on left and right when using this code? – android developer Apr 30 '17 at 23:13
  • From my tests, this doesn't really work ... The byte buffer from the `ImageReader` is not in JPEG format, it is in a raw format that `BitmapFactory.decodeByteArray` is smart enough to detect and understand, that is why the image appears at all. – ianribas May 02 '17 at 20:15
8

I tested the code of the first answer, but unfortunately it does not work on real device. I make some investigation and the following code solved my problem:

 mImgReader = ImageReader.newInstance(mWidth, mHeight, PixelFormat.RGBA_8888, 5);
    mSurface = mImgReader.getSurface();// mSurfaceView.getHolder().getSurface();
    mImgReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Log.i(TAG, "in OnImageAvailable");
            FileOutputStream fos = null;
            Bitmap bitmap = null;
            Image img = null;
            try {
                img = reader.acquireLatestImage();
                if (img != null) {
                    Image.Plane[] planes = img.getPlanes();
                    if (planes[0].getBuffer() == null) {
                        return;
                    }
                    int width = img.getWidth();
                    int height = img.getHeight();
                    int pixelStride = planes[0].getPixelStride();
                    int rowStride = planes[0].getRowStride();
                    int rowPadding = rowStride - pixelStride * width;
                    byte[] newData = new byte[width * height * 4];

                    int offset = 0;
                    bitmap = Bitmap.createBitmap(metrics,width, height, Bitmap.Config.ARGB_8888);
                    ByteBuffer buffer = planes[0].getBuffer();
                    for (int i = 0; i < height; ++i) {
                        for (int j = 0; j < width; ++j) {
                            int pixel = 0;
                            pixel |= (buffer.get(offset) & 0xff) << 16;     // R
                            pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
                            pixel |= (buffer.get(offset + 2) & 0xff);       // B
                            pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
                            bitmap.setPixel(j, i, pixel);
                            offset += pixelStride;
                        }
                        offset += rowPadding;
                    }
                    String name = "/myscreen" + count + ".png";
                    count++;
                    File file = new File(Environment.getExternalStorageDirectory(), name);
                    fos = new FileOutputStream(file);
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
                    Log.i(TAG, "image saved in" + Environment.getExternalStorageDirectory() + name);
                    img.close();
                }

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (null != fos) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (null != bitmap) {
                    bitmap.recycle();
                }
                if (null != img) {
                    img.close();
                }

            }



        }
    }, mHandler);
Charlesjean
  • 566
  • 1
  • 5
  • 16
  • Will give it a try when I get back from holidays for sure. Can you please tell me which device you used and how many bitmaps/sec are you able to create prior to storing them on external storage (i.e. prior to line `String name = "/myscreen" + count + ".png";`? Many thanks in advance! – mtsahakis Dec 26 '14 at 19:55
  • My device is nexus5, the framerate is not good even I remove these CPU busy computation. I do not know why. – Charlesjean Dec 28 '14 at 01:17
  • 1
    This was reported to me by other users trying my code on a test project I posted on github https://github.com/mtsahakis/MediaProjectionDemo. I changed the code from `ImageReader.newInstance(width, height, ImageFormat.JPEG, 5);` to `ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 5);` as I believe solves the issue on certain devices. Also on my test device (Nexus 7 with Android 5.0.2) the rate capture improves a bit. – mtsahakis Feb 09 '15 at 11:18
  • 1
    @mtsahakis - I was facing similar issues. Changing image format to PixelFormat.RGBA_8888 Thanks. – Amit Gupta Jun 06 '15 at 07:06
  • This can be simplified a bit by using `Color.argb`. – Blaisorblade Sep 11 '16 at 11:08
  • Did anyone find a solution to improve framerate? – user2801184 Nov 28 '16 at 07:32
  • 5
    What is metrics supposed to be? – Daniel Viglione Jul 08 '17 at 00:49
  • Sample code working. But it too slow. 99 seconds required for only single shot processing. – hurelhuyag Jul 09 '18 at 07:32
  • are you sure it's this sample code cost so much time ? I use this code in my project with no performance issue. – Charlesjean Jul 26 '18 at 11:57
4

In case someone else stumbles on this, working code is as follows:

            mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 5);
            mProjection.createVirtualDisplay("test", width, height, density, flags, mImageReader.getSurface(), new VirtualDisplayCallback(), mHandler);
            mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {

                @Override
                public void onImageAvailable(ImageReader reader) {
                    Image image = null;
                    FileOutputStream fos = null;
                    Bitmap bitmap = null;

                    try {
                        image = mImageReader.acquireLatestImage();
                        fos = new FileOutputStream(getFilesDir() + "/myscreen.jpg");
                        final Image.Plane[] planes = image.getPlanes();
                        final Buffer buffer = planes[0].getBuffer().rewind();
                        bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                        bitmap.copyPixelsFromBuffer(buffer);
                        bitmap.compress(CompressFormat.JPEG, 100, fos);

                    } catch (Exception e) {
                        e.printStackTrace();

                        if (image!=null)
                            image.close();
                    } finally {
                        if (fos!=null) {
                            try {
                                fos.close();
                            } catch (IOException ioe) { 
                                ioe.printStackTrace();
                            }
                        }

                        if (bitmap!=null)
                            bitmap.recycle();
                    }
                }

            }, mHandler);

As you see I am saving the bitmap captured from ImageReader to a fileoutput stream and this produces a valid jpeg file.

The messages I was getting back from android_media_ImageReader.cpp were not indicating any sort of misbehaviour.

Hope it helps someone in the future!

mtsahakis
  • 613
  • 1
  • 8
  • 16
  • Thank for you answer. I have been stuck on this for a while, and still didn't manage to get it to work. What emulator are you using? What are the flags you are settings? – Alessandro Roaro Dec 12 '14 at 20:59
  • Hi Alessandro. So, the flags I am setting are `final int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;`. The emulator I am using is pretty standard, with armeabi-v7a CPU, 1024MB RAM, 32MB VM heap and 1024MB internal storage. I am waiting to test it on a real device when I get my hands on a Lollipop one :) – mtsahakis Dec 15 '14 at 09:03
  • Then I really don't know what am I doing wrong because those are the same flags I set. The width and height you are using are from the screen size? Tomorrow I will test it on a Lollipop device and I will let you know. – Alessandro Roaro Dec 15 '14 at 10:07
  • Hey Alessandro, really sorry to see that your code is not working. My code on width and height is really straightforward: `final Display display = getWindowManager().getDefaultDisplay(); final Point size = new Point(); display.getSize(size); final int width = size.x; final int height = size.y;`. Perhaps later today I will put in on github for you to have a look. Best of luck! – mtsahakis Dec 15 '14 at 14:08
  • Hey Alessandro, I added my code on github, have a look at https://github.com/mtsahakis/MediaProjectionDemo. Its pretty straightforward but if you run to any trouble let me know. – mtsahakis Dec 16 '14 at 09:33
  • @mtsahakis do you try your code on a real device? It not work on my nexus 5 with android 5.0 – Charlesjean Dec 25 '14 at 15:43
4

I tried to use many example code. But none of those codes are working. I mixed those codes myself. And I created working sample code.

    final DisplayMetrics dm = getResources().getDisplayMetrics();
    mImageReader = ImageReader.newInstance(dm.widthPixels, dm.heightPixels, PixelFormat.RGBA_8888, 1);
    mProjection.createVirtualDisplay("screen-mirror", dm.widthPixels, dm.heightPixels, dm.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), vdCallback, mHandler);
    mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = reader.acquireLatestImage();
            if (image == null){
                return;
            }
            final Image.Plane[] planes = image.getPlanes();
            final ByteBuffer buffer = planes[0].getBuffer();
            int offset = 0;
            int pixelStride = planes[0].getPixelStride();
            int rowStride = planes[0].getRowStride();
            int rowPadding = rowStride - pixelStride * dm.widthPixels;
            // create bitmap
            Bitmap bitmap = Bitmap.createBitmap(dm.widthPixels+rowPadding/pixelStride, dm.heightPixels, Bitmap.Config.ARGB_8888);
            bitmap.copyPixelsFromBuffer(buffer);
            imageView.setImageBitmap(bitmap);
            image.close();
        }
    }, mHandler);
hurelhuyag
  • 1,412
  • 1
  • 15
  • 20
0

Fo those of you looking for the easiest way to capture from ImageReader to a JPG (be it bytes on disk or in memory) It's actually quite simple.

Given an ImageReader, set it to ImageFormat.JPEG:

ImageReader imageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), imageFormat, 1);

Then in the CameraCaptureSession.StateCallback:

captureSession.capture(captureRequest, new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);             
        Image image = imageReader.acquireLatestImage();
        final Image.Plane[] planes = image.getPlanes();
        final ByteBuffer buffer = planes[0].getBuffer();
        buffer.rewind();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);

    }
}

Then you have bytes which you can do anything with. Send them over a socket, to a file, hash them, etc.

user9170
  • 860
  • 7
  • 16