8

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
  • 793
  • 1
  • 8
  • 18
  • 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

14

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
  • 13,220
  • 11
  • 56
  • 69
  • 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,106
  • 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
  • To me, PixelFormat is reported as unsupported. ImageFormat suggested instead. And ImageFormat does NOT have the common portable formats except of JPEG, which in turn is very ineffective (slow). Most of ImageFormat is some proprietary crap which cannot easily be displayed and additionally which is device specific. – Jiří Křivánek Apr 20 '23 at 07:32
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
  • 793
  • 1
  • 8
  • 18
  • 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
  • Yes, this works, but I cannot upvote it for two reasons: It is very slow + On some devices (certain Qualcom chipsets), it causes the entire camera 2 facility to crash and the only way how to recover is the device reboot... – Jiří Křivánek Apr 20 '23 at 07:35
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,691
  • 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
  • 950
  • 9
  • 18