Search code examples
xamarinxamarin.androidandroid-camera2

Xamarin Cam2 IOnImageAvailableListener's OnImageAvailable called twice causing


UPDATE: The initial question has been answered as to why the crashes happen but the lingering problem remains of why is the 'OnImageAvailable' callback called so may times? When it is called, I want to do stuff with the image, but whatever method I run at that time is called many times. Is this the wrong place to be using the resulting image?


I am using the sample code found here for a Xamarin Android implementation of the Android Camera2 API. My issue is that when the capture button is pressed a single time, the OnCameraAvalibleListener's OnImageAvailable callback gets called multiple times.

This is causing a problem because the image from AcquireNextImage needs to be closed before another can be used, but close is not called until the Run method of the ImageSaver class as seen below.

This causes these 2 errors:

Unable to acquire a buffer item, very likely client tried to acquire more than maxImages buffers

AND

maxImages (2) has already been acquired, call #close before acquiring more.

The max image is set to 2 by default, but setting it to 1 does not help. How do I prevent the callback from being called twice?

public void OnImageAvailable(ImageReader reader)
    {
        var image = reader.AcquireNextImage();
        owner.mBackgroundHandler.Post(new ImageSaver(image, file));
    }

    // Saves a JPEG {@link Image} into the specified {@link File}.
    private class ImageSaver : Java.Lang.Object, IRunnable
    {
        // The JPEG image
        private Image mImage;

        // The file we save the image into.
        private File mFile;

        public ImageSaver(Image image, File file)
        {
            if (image == null)
                throw new System.ArgumentNullException("image");
            if (file == null)
                throw new System.ArgumentNullException("file");

            mImage = image;
            mFile = file;
        }

        public void Run()
        {
            ByteBuffer buffer = mImage.GetPlanes()[0].Buffer;
            byte[] bytes = new byte[buffer.Remaining()];
            buffer.Get(bytes);
            using (var output = new FileOutputStream(mFile))
            {
                try
                {
                    output.Write(bytes);
                }
                catch (IOException e)
                {
                    e.PrintStackTrace();
                }
                finally
                {
                    mImage.Close();
                }
            }
        }
    }

Solution

  • The method OnImageAvailable can be called again as soon as you leave it if there is another picture in the pipeline.

    I would recommend calling Close in the same method you are calling AcquireNextImage. So, if you choose to get the image directly from that callback, then you have to call Close in there as well.

    One solution involved grabbing the image in that method and close it right away.

    public void OnImageAvailable(ImageReader reader)
    {
        var image = reader.AcquireNextImage();
    
        try
        {
            ByteBuffer buffer = mImage.GetPlanes()[0].Buffer;
            byte[] bytes = new byte[buffer.Remaining()];
            buffer.Get(bytes);
    
            // I am not sure where you get the file instance but it is not important.
            owner.mBackgroundHandler.Post(new ImageSaver(bytes, file));
        }
        finally
        {
            image.Close();
        }
    }
    

    The ImageSaver would be modified to accept the byte array as first parameter in the constructor:

    public ImageSaver(byte[] bytes, File file)
    {
        if (bytes == null)
            throw new System.ArgumentNullException("bytes");
        if (file == null)
            throw new System.ArgumentNullException("file");
    
        mBytes = bytes;
        mFile = file;
    }
    

    The major downside of this solution is the risk of putting a lot of pressure on the memory as you basically save the images in memory until they are processed, one after another.

    Another solution consists in acquiring the image on the background thread instead.

    public void OnImageAvailable(ImageReader reader)
    {
        // Again, I am not sure where you get the file instance but it is not important.
        owner.mBackgroundHandler.Post(new ImageSaver(reader, file));
    }
    

    This solution is less intensive on the memory; but you might have to increase the maximum number of images from 2 to something higher depending on your needs. Again, the ImageSaver's constructor needs to be modified to accept an ImageReader as a parameter:

    public ImageSaver(ImageReader imageReader, File file)
    {
        if (imageReader == null)
            throw new System.ArgumentNullException("imageReader");
        if (file == null)
            throw new System.ArgumentNullException("file");
    
        mImageReader = imageReader;
        mFile = file;
    }
    

    Now the Run method would have the responsibility of acquiring and releasing the Image:

    public void Run()
    {
        Image image = mImageReader.AcquireNextImage();
    
        try
        {
            ByteBuffer buffer = image.GetPlanes()[0].Buffer;
            byte[] bytes = new byte[buffer.Remaining()];
            buffer.Get(bytes);
    
            using (var output = new FileOutputStream(mFile))
            {
                try
                {
                    output.Write(bytes);
                }
                catch (IOException e)
                {
                    e.PrintStackTrace();
                }
            }
        }
        finally
        {
            image?.Close();
        }
    }