diff options
author | Igor Murashkin <iam@google.com> | 2013-08-29 15:38:17 -0700 |
---|---|---|
committer | Igor Murashkin <iam@google.com> | 2013-09-12 16:57:57 -0700 |
commit | 5e712064dfe48992f8f732208fa4fc13f3455b30 (patch) | |
tree | 5b94b96e4dc152d88cc6b0cdb678f004e4d25cd5 | |
parent | e850c973b0662975137cee8a05f8ee2cb82d9b2a (diff) | |
download | frameworks_base-5e712064dfe48992f8f732208fa4fc13f3455b30.zip frameworks_base-5e712064dfe48992f8f732208fa4fc13f3455b30.tar.gz frameworks_base-5e712064dfe48992f8f732208fa4fc13f3455b30.tar.bz2 |
media: Update ImageReader APIs
Bug: 10461757
Change-Id: Ic04e4c41965e3d417b29004f3f08e0cd56b8f4cb
7 files changed, 548 insertions, 118 deletions
diff --git a/api/current.txt b/api/current.txt index 1a9b77c..b0a18de 100644 --- a/api/current.txt +++ b/api/current.txt @@ -12332,7 +12332,7 @@ package android.media { field public static final int EULER_Z = 2; // 0x2 } - public abstract interface Image implements java.lang.AutoCloseable { + public abstract class Image implements java.lang.AutoCloseable { method public abstract void close(); method public abstract int getFormat(); method public abstract int getHeight(); @@ -12341,23 +12341,30 @@ package android.media { method public abstract int getWidth(); } - public static abstract interface Image.Plane { + public static abstract class Image.Plane { method public abstract java.nio.ByteBuffer getBuffer(); method public abstract int getPixelStride(); method public abstract int getRowStride(); } - public final class ImageReader implements java.lang.AutoCloseable { - ctor public ImageReader(int, int, int, int); + public class ImageReader implements java.lang.AutoCloseable { + method public android.media.Image acquireLatestImage() throws android.media.ImageReader.MaxImagesAcquiredException; + method public android.media.Image acquireNextImage() throws android.media.ImageReader.MaxImagesAcquiredException; method public void close(); method public int getHeight(); method public int getImageFormat(); method public int getMaxImages(); - method public android.media.Image getNextImage(); method public android.view.Surface getSurface(); method public int getWidth(); - method public void releaseImage(android.media.Image); - method public void setImageAvailableListener(android.media.ImageReader.OnImageAvailableListener, android.os.Handler); + method public static android.media.ImageReader newInstance(int, int, int, int); + method public void setOnImageAvailableListener(android.media.ImageReader.OnImageAvailableListener, android.os.Handler); + } + + public static class ImageReader.MaxImagesAcquiredException extends java.lang.Exception { + ctor public ImageReader.MaxImagesAcquiredException(); + ctor public ImageReader.MaxImagesAcquiredException(java.lang.String); + ctor public ImageReader.MaxImagesAcquiredException(java.lang.String, java.lang.Throwable); + ctor public ImageReader.MaxImagesAcquiredException(java.lang.Throwable); } public static abstract interface ImageReader.OnImageAvailableListener { diff --git a/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java b/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java index ebecf2e..8278579 100644 --- a/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java +++ b/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java @@ -26,6 +26,7 @@ import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.media.Image; import android.media.ImageReader; +import android.media.ImageReader.MaxImagesAcquiredException; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -83,8 +84,8 @@ public class VirtualDisplayTest extends AndroidTestCase { mImageReaderLock.lock(); try { - mImageReader = new ImageReader(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2); - mImageReader.setImageAvailableListener(mImageListener, mHandler); + mImageReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2); + mImageReader.setOnImageAvailableListener(mImageListener, mHandler); mSurface = mImageReader.getSurface(); } finally { mImageReaderLock.unlock(); @@ -409,19 +410,11 @@ public class VirtualDisplayTest extends AndroidTestCase { } Log.d(TAG, "New image available from virtual display."); - Image image = reader.getNextImage(); + + // Get the latest buffer. + Image image = reader.acquireLatestImage(); if (image != null) { try { - // Get the latest buffer. - for (;;) { - Image nextImage = reader.getNextImage(); - if (nextImage == null) { - break; - } - reader.releaseImage(image); - image = nextImage; - } - // Scan for colors. int color = scanImage(image); synchronized (this) { @@ -431,9 +424,12 @@ public class VirtualDisplayTest extends AndroidTestCase { } } } finally { - reader.releaseImage(image); + image.close(); } } + } catch (MaxImagesAcquiredException e) { + // We should never try to consume more buffers than maxImages. + throw new IllegalStateException(e); } finally { mImageReaderLock.unlock(); } diff --git a/media/java/android/media/Image.java b/media/java/android/media/Image.java index 9f442f5..915b09e 100644 --- a/media/java/android/media/Image.java +++ b/media/java/android/media/Image.java @@ -16,20 +16,19 @@ package android.media; -import android.graphics.ImageFormat; import java.nio.ByteBuffer; import java.lang.AutoCloseable; /** * <p>A single complete image buffer to use with a media source such as a * {@link MediaCodec} or a - * {@link android.hardware.camera2.CameraDevice}.</p> + * {@link android.hardware.camera2.CameraDevice CameraDevice}.</p> * * <p>This class allows for efficient direct application access to the pixel * data of the Image through one or more * {@link java.nio.ByteBuffer ByteBuffers}. Each buffer is encapsulated in a * {@link Plane} that describes the layout of the pixel data in that plane. Due - * to this direct access, and unlike the {@link android.graphics.Bitmap} class, + * to this direct access, and unlike the {@link android.graphics.Bitmap Bitmap} class, * Images are not directly usable as as UI resources.</p> * * <p>Since Images are often directly produced or consumed by hardware @@ -40,19 +39,29 @@ import java.lang.AutoCloseable; * from various media sources, not closing old Image objects will prevent the * availability of new Images once * {@link ImageReader#getMaxImages the maximum outstanding image count} is - * reached.</p> + * reached. When this happens, the function acquiring new Images will typically + * throw a + * {@link ImageReader.MaxImagesAcquiredException MaxImagesAcquiredException}.</p> * * @see ImageReader */ -public interface Image extends AutoCloseable { +public abstract class Image implements AutoCloseable { + /** + * @hide + */ + protected Image() { + } + /** * Get the format for this image. This format determines the number of * ByteBuffers needed to represent the image, and the general layout of the * pixel data in each in ByteBuffer. * + * <p> * The format is one of the values from - * {@link android.graphics.ImageFormat}. The mapping between the formats and - * the planes is as follows: + * {@link android.graphics.ImageFormat ImageFormat}. The mapping between the + * formats and the planes is as follows: + * </p> * * <table> * <tr> @@ -61,13 +70,14 @@ public interface Image extends AutoCloseable { * <th>Layout details</th> * </tr> * <tr> - * <td>{@link android.graphics.ImageFormat#JPEG}</td> + * <td>{@link android.graphics.ImageFormat#JPEG JPEG}</td> * <td>1</td> * <td>Compressed data, so row and pixel strides are 0. To uncompress, use - * {@link android.graphics.BitmapFactory#decodeByteArray}.</td> + * {@link android.graphics.BitmapFactory#decodeByteArray BitmapFactory#decodeByteArray}. + * </td> * </tr> * <tr> - * <td>{@link android.graphics.ImageFormat#YUV_420_888}</td> + * <td>{@link android.graphics.ImageFormat#YUV_420_888 YUV_420_888}</td> * <td>3</td> * <td>A luminance plane followed by the Cb and Cr chroma planes. * The chroma planes have half the width and height of the luminance @@ -75,53 +85,60 @@ public interface Image extends AutoCloseable { * Each plane has its own row stride and pixel stride.</td> * </tr> * <tr> - * <td>{@link android.graphics.ImageFormat#RAW_SENSOR}</td> + * <td>{@link android.graphics.ImageFormat#RAW_SENSOR RAW_SENSOR}</td> * <td>1</td> * <td>A single plane of raw sensor image data, with 16 bits per color * sample. The details of the layout need to be queried from the source of * the raw sensor data, such as - * {@link android.hardware.camera2.CameraDevice}. + * {@link android.hardware.camera2.CameraDevice CameraDevice}. * </td> * </tr> * </table> * * @see android.graphics.ImageFormat */ - public int getFormat(); + public abstract int getFormat(); /** * The width of the image in pixels. For formats where some color channels * are subsampled, this is the width of the largest-resolution plane. */ - public int getWidth(); + public abstract int getWidth(); /** * The height of the image in pixels. For formats where some color channels * are subsampled, this is the height of the largest-resolution plane. */ - public int getHeight(); + public abstract int getHeight(); /** - * Get the timestamp associated with this frame. The timestamp is measured - * in nanoseconds, and is monotonically increasing. However, the zero point - * and whether the timestamp can be compared against other sources of time - * or images depend on the source of this image. + * Get the timestamp associated with this frame. + * <p> + * The timestamp is measured in nanoseconds, and is monotonically + * increasing. However, the zero point and whether the timestamp can be + * compared against other sources of time or images depend on the source of + * this image. + * </p> */ - public long getTimestamp(); + public abstract long getTimestamp(); /** * Get the array of pixel planes for this Image. The number of planes is * determined by the format of the Image. */ - public Plane[] getPlanes(); + public abstract Plane[] getPlanes(); /** - * Free up this frame for reuse. After calling this method, calling any - * methods on this Image will result in an IllegalStateException, and - * attempting to read from ByteBuffers returned by an earlier - * {@code Plane#getBuffer} call will have undefined behavior. + * Free up this frame for reuse. + * <p> + * After calling this method, calling any methods on this {@code Image} will + * result in an {@link IllegalStateException}, and attempting to read from + * {@link ByteBuffer ByteBuffers} returned by an earlier + * {@link Plane#getBuffer} call will have undefined behavior. + * </p> */ - public void close(); + @Override + public abstract void close(); /** * <p>A single color plane of image data.</p> @@ -134,29 +151,41 @@ public interface Image extends AutoCloseable { * * @see #getFormat */ - public interface Plane { + public static abstract class Plane { /** - * <p>The row stride for this color plane, in bytes. + * @hide + */ + protected Plane() { + } + + /** + * <p>The row stride for this color plane, in bytes.</p> * * <p>This is the distance between the start of two consecutive rows of - * pixels in the image.</p> + * pixels in the image. The row stride is always greater than 0.</p> */ - public int getRowStride(); + public abstract int getRowStride(); /** * <p>The distance between adjacent pixel samples, in bytes.</p> * * <p>This is the distance between two consecutive pixel values in a row * of pixels. It may be larger than the size of a single pixel to - * account for interleaved image data or padded formats.</p> + * account for interleaved image data or padded formats. + * The pixel stride is always greater than 0.</p> */ - public int getPixelStride(); + public abstract int getPixelStride(); /** - * <p>Get a set of direct {@link java.nio.ByteBuffer byte buffers} + * <p>Get a direct {@link java.nio.ByteBuffer ByteBuffer} * containing the frame data.</p> * + * <p>In particular, the buffer returned will always have + * {@link java.nio.ByteBuffer#isDirect isDirect} return {@code true}, so + * the underlying data could be mapped as a pointer in JNI without doing + * any copies with {@code GetDirectBufferAddress}.</p> + * * @return the byte buffer containing the image data for this plane. */ - public ByteBuffer getBuffer(); + public abstract ByteBuffer getBuffer(); } } diff --git a/media/java/android/media/ImageReader.java b/media/java/android/media/ImageReader.java index b14a899..e5bcb3e 100644 --- a/media/java/android/media/ImageReader.java +++ b/media/java/android/media/ImageReader.java @@ -40,41 +40,90 @@ import java.nio.ByteOrder; * <p>The image data is encapsulated in {@link Image} objects, and multiple such * objects can be accessed at the same time, up to the number specified by the * {@code maxImages} constructor parameter. New images sent to an ImageReader - * through its Surface are queued until accessed through the - * {@link #getNextImage} call. Due to memory limits, an image source will + * through its {@link Surface} are queued until accessed through the {@link #acquireLatestImage} + * or {@link #acquireNextImage} call. Due to memory limits, an image source will * eventually stall or drop Images in trying to render to the Surface if the * ImageReader does not obtain and release Images at a rate equal to the * production rate.</p> */ -public final class ImageReader implements AutoCloseable { +public class ImageReader implements AutoCloseable { + + /** + * <p> + * This exception is thrown when the user of an {@link ImageReader} tries to acquire a new + * {@link Image} when the maximum number of {@link Image Images} have already been acquired. + * The maximum number is determined by the {@code maxBuffers} argument of + * {@link ImageReader#newInstance newInstance}. + * </p> + * + * <p> + * To recover from this exception, release existing {@link Image images} back to the + * reader with {@link Image#close}. + * </p> + * + * @see Image#close + * @see ImageReader#acquireLatestImage + * @see ImageReader#acquireNextImage + */ + public static class MaxImagesAcquiredException extends Exception { + /** + * Suppress Eclipse warnings + */ + private static final long serialVersionUID = 761231231236L; + + public MaxImagesAcquiredException() { + } + + public MaxImagesAcquiredException(String message) { + super(message); + } + + public MaxImagesAcquiredException(String message, Throwable throwable) { + super(message, throwable); + } + + public MaxImagesAcquiredException(Throwable throwable) { + super(throwable); + } + } /** * <p>Create a new reader for images of the desired size and format.</p> * - * <p>The maxImages parameter determines the maximum number of {@link Image} - * objects that can be be acquired from the ImageReader + * <p>The {@code maxImages} parameter determines the maximum number of {@link Image} + * objects that can be be acquired from the {@code ImageReader} * simultaneously. Requesting more buffers will use up more memory, so it is * important to use only the minimum number necessary for the use case.</p> * * <p>The valid sizes and formats depend on the source of the image * data.</p> * - * @param width the width in pixels of the Images that this reader will - * produce. - * @param height the height in pixels of the Images that this reader will - * produce. - * @param format the format of the Image that this reader will produce. This - * must be one of the {@link android.graphics.ImageFormat} or - * {@link android.graphics.PixelFormat} constants. - * @param maxImages the maximum number of images the user will want to - * access simultaneously. This should be as small as possible to limit - * memory use. Once maxImages Images are obtained by the user, one of them - * has to be released before a new Image will become available for access - * through getNextImage(). Must be greater than 0. + * @param width + * The width in pixels of the Images that this reader will produce. + * @param height + * The height in pixels of the Images that this reader will produce. + * @param format + * The format of the Image that this reader will produce. This + * must be one of the {@link android.graphics.ImageFormat} or + * {@link android.graphics.PixelFormat} constants. + * @param maxImages + * The maximum number of images the user will want to + * access simultaneously. This should be as small as possible to limit + * memory use. Once maxImages Images are obtained by the user, one of them + * has to be released before a new Image will become available for access + * through {@link #acquireLatestImage()} or {@link #acquireNextImage()}. + * Must be greater than 0. * * @see Image */ - public ImageReader(int width, int height, int format, int maxImages) { + public static ImageReader newInstance(int width, int height, int format, int maxImages) { + return new ImageReader(width, height, format, maxImages); + } + + /** + * @hide + */ + protected ImageReader(int width, int height, int format, int maxImages) { mWidth = width; mHeight = height; mFormat = format; @@ -96,33 +145,79 @@ public final class ImageReader implements AutoCloseable { mSurface = nativeGetSurface(); } + /** + * The width of each {@link Image}, in pixels. + * + * <p>ImageReader guarantees that all Images acquired from ImageReader (for example, with + * {@link #acquireNextImage}) will have the same dimensions as specified in + * {@link #newInstance}.</p> + * + * @return the width of an Image + */ public int getWidth() { return mWidth; } + /** + * The height of each {@link Image}, in pixels. + * + * <p>ImageReader guarantees that all Images acquired from ImageReader (for example, with + * {@link #acquireNextImage}) will have the same dimensions as specified in + * {@link #newInstance}.</p> + * + * @return the height of an Image + */ public int getHeight() { return mHeight; } + /** + * The {@link ImageFormat image format} of each Image. + * + * <p>ImageReader guarantees that all {@link Image Images} acquired from ImageReader + * (for example, with {@link #acquireNextImage}) will have the same format as specified in + * {@link #newInstance}.</p> + * + * @return the format of an Image + * + * @see ImageFormat + */ public int getImageFormat() { return mFormat; } + /** + * Maximum number of images that can be acquired from the ImageReader by any time (for example, + * with {@link #acquireNextImage}). + * + * <p>An image is considered acquired after it's returned by a function from ImageReader, and + * until the Image is {@link Image#close closed} to release the image back to the ImageReader. + * </p> + * + * <p>Attempting to acquire more than {@code maxImages} concurrently will result in the + * acquire function throwing a {@link MaxImagesAcquiredException}. Furthermore, + * while the max number of images have been acquired by the ImageReader user, the producer + * enqueueing additional images may stall until at least one image has been released. </p> + * + * @return Maximum number of images for this ImageReader. + * + * @see Image#close + */ public int getMaxImages() { return mMaxImages; } /** - * <p>Get a Surface that can be used to produce Images for this - * ImageReader.</p> + * <p>Get a {@link Surface} that can be used to produce {@link Image Images} for this + * {@code ImageReader}.</p> * - * <p>Until valid image data is rendered into this Surface, the - * {@link #getNextImage} method will return {@code null}. Only one source + * <p>Until valid image data is rendered into this {@link Surface}, the + * {@link #acquireNextImage} method will return {@code null}. Only one source * can be producing data into this Surface at the same time, although the - * same Surface can be reused with a different API once the first source is - * disconnected from the Surface.</p> + * same {@link Surface} can be reused with a different API once the first source is + * disconnected from the {@link Surface}.</p> * - * @return A Surface to use for a drawing target for various APIs. + * @return A {@link Surface} to use for a drawing target for various APIs. */ public Surface getSurface() { return mSurface; @@ -130,27 +225,96 @@ public final class ImageReader implements AutoCloseable { /** * <p> - * Get the next Image from the ImageReader's queue. Returns {@code null} if + * Acquire the latest {@link Image} from the ImageReader's queue, dropping older + * {@link Image images}. Returns {@code null} if no new image is available. + * </p> + * <p> + * This operation will acquire all the images possible from the ImageReader, + * but {@link #close} all images that aren't the latest. This function is + * recommended to use over {@link #acquireNextImage} for most use-cases, as it's + * more suited for real-time processing. + * </p> + * <p> + * Note that {@link #getMaxImages maxImages} should be at least 2 for + * {@link #acquireLatestImage} to be any different than {@link #acquireNextImage} - + * discarding all-but-the-newest {@link Image} requires temporarily acquiring two + * {@link Image Images} at once. Or more generally, calling {@link #acquireLatestImage} + * with less than two images of margin, that is + * {@code (maxImages - currentAcquiredImages < 2)} will not discard as expected. + * </p> + * <p> + * This operation will fail by throwing an {@link MaxImagesAcquiredException} if + * {@code maxImages} have been acquired with {@link #acquireLatestImage} or + * {@link #acquireNextImage}. In particular a sequence of {@link #acquireLatestImage} + * calls greater than {@link #getMaxImages} without calling {@link Image#close} in-between + * will exhaust the underlying queue. At such a time, {@link MaxImagesAcquiredException} + * will be thrown until more images are + * released with {@link Image#close}. + * </p> + * + * @return latest frame of image data, or {@code null} if no image data is available. + * @throws MaxImagesAcquiredException if too many images are currently acquired + */ + public Image acquireLatestImage() throws MaxImagesAcquiredException { + Image image = acquireNextImage(); + if (image == null) { + return null; + } + try { + for (;;) { + Image next = acquireNextImageNoThrow(); + if (next == null) { + Image result = image; + image = null; + return result; + } + image.close(); + image = next; + } + } finally { + if (image != null) { + image.close(); + } + } + } + + private Image acquireNextImageNoThrow() { + try { + return acquireNextImage(); + } catch (MaxImagesAcquiredException ex) { + return null; + } + } + + /** + * <p> + * Acquire the next Image from the ImageReader's queue. Returns {@code null} if * no new image is available. * </p> + * + * <p><i>Warning:</i> Consider using {@link #acquireLatestImage()} instead, as it will + * automatically release older images, and allow slower-running processing routines to catch + * up to the newest frame. Usage of {@link #acquireNextImage} is recommended for + * batch/background processing. Incorrectly using this function can cause images to appear + * with an ever-increasing delay, followed by a complete stall where no new images seem to + * appear. + * </p> + * * <p> - * This operation will fail by throwing an - * {@link Surface.OutOfResourcesException OutOfResourcesException} if too - * many images have been acquired with {@link #getNextImage}. In particular - * a sequence of {@link #getNextImage} calls greater than {@link #getMaxImages} - * without calling {@link Image#close} or {@link #releaseImage} in-between - * will exhaust the underlying queue. At such a time, - * {@link Surface.OutOfResourcesException OutOfResourcesException} will be - * thrown until more images are released with {@link Image#close} or - * {@link #releaseImage}. + * This operation will fail by throwing an {@link MaxImagesAcquiredException} if + * {@code maxImages} have been acquired with {@link #acquireNextImage} or + * {@link #acquireLatestImage}. In particular a sequence of {@link #acquireNextImage} or + * {@link #acquireLatestImage} calls greater than {@link #getMaxImages maxImages} without + * calling {@link Image#close} in-between will exhaust the underlying queue. At such a time, + * {@link MaxImagesAcquiredException} will be thrown until more images are released with + * {@link Image#close}. * </p> * - * @return a new frame of image data, or {@code null} if no image data is - * available. - * @throws Surface.OutOfResourcesException if too many images are currently - * acquired + * @return a new frame of image data, or {@code null} if no image data is available. + * @throws MaxImagesAcquiredException if {@code maxImages} images are currently acquired + * @see #acquireLatestImage */ - public Image getNextImage() { + public Image acquireNextImage() throws MaxImagesAcquiredException { SurfaceImage si = new SurfaceImage(); if (nativeImageSetup(si)) { // create SurfacePlane objects @@ -164,7 +328,7 @@ public final class ImageReader implements AutoCloseable { /** * <p>Return the frame to the ImageReader for reuse.</p> */ - public void releaseImage(Image i) { + private void releaseImage(Image i) { if (! (i instanceof SurfaceImage) ) { throw new IllegalArgumentException( "This image was not produced by an ImageReader"); @@ -183,13 +347,16 @@ public final class ImageReader implements AutoCloseable { /** * Register a listener to be invoked when a new image becomes available * from the ImageReader. - * @param listener the listener that will be run - * @param handler The handler on which the listener should be invoked, or null - * if the listener should be invoked on the calling thread's looper. * - * @throws IllegalArgumentException if no handler specified and the calling thread has no looper + * @param listener + * The listener that will be run. + * @param handler + * The handler on which the listener should be invoked, or null + * if the listener should be invoked on the calling thread's looper. + * @throws IllegalArgumentException + * If no handler specified and the calling thread has no looper. */ - public void setImageAvailableListener(OnImageAvailableListener listener, Handler handler) { + public void setOnImageAvailableListener(OnImageAvailableListener listener, Handler handler) { mImageListener = listener; Looper looper; @@ -206,12 +373,16 @@ public final class ImageReader implements AutoCloseable { /** * Callback interface for being notified that a new image is available. + * + * <p> * The onImageAvailable is called per image basis, that is, callback fires for every new frame * available from ImageReader. + * </p> */ public interface OnImageAvailableListener { /** * Callback that is called when a new image is available from ImageReader. + * * @param reader the ImageReader the callback is associated with. * @see ImageReader * @see Image @@ -220,12 +391,17 @@ public final class ImageReader implements AutoCloseable { } /** - * Free up all the resources associated with this ImageReader. After - * Calling this method, this ImageReader can not be used. calling - * any methods on this ImageReader and Images previously provided by {@link #getNextImage} - * will result in an IllegalStateException, and attempting to read from - * ByteBuffers returned by an earlier {@code Plane#getBuffer} call will + * Free up all the resources associated with this ImageReader. + * + * <p> + * After calling this method, this ImageReader can not be used. Calling + * any methods on this ImageReader and Images previously provided by + * {@link #acquireNextImage} or {@link #acquireLatestImage} + * will result in an {@link IllegalStateException}, and attempting to read from + * {@link ByteBuffer ByteBuffers} returned by an earlier + * {@link Image.Plane#getBuffer Plane#getBuffer} call will * have undefined behavior. + * </p> */ @Override public void close() { @@ -242,11 +418,14 @@ public final class ImageReader implements AutoCloseable { } /** - * Only a subset of the formats defined in {@link android.graphics.ImageFormat} and - * {@link android.graphics.PixelFormat} are supported by ImageReader. When reading RGB - * data from a surface, the formats defined in {@link android.graphics.PixelFormat} - * can be used, when reading YUV, JPEG or raw sensor data ( for example, from camera - * or video decoder), formats from {@link android.graphics.ImageFormat} are used. + * Only a subset of the formats defined in + * {@link android.graphics.ImageFormat ImageFormat} and + * {@link android.graphics.PixelFormat PixelFormat} are supported by + * ImageReader. When reading RGB data from a surface, the formats defined in + * {@link android.graphics.PixelFormat PixelFormat} can be used, when + * reading YUV, JPEG or raw sensor data (for example, from camera or video + * decoder), formats from {@link android.graphics.ImageFormat ImageFormat} + * are used. */ private int getNumPlanesFromFormat() { switch (mFormat) { @@ -308,7 +487,7 @@ public final class ImageReader implements AutoCloseable { */ private long mNativeContext; - private class SurfaceImage implements android.media.Image { + private class SurfaceImage extends android.media.Image { public SurfaceImage() { mIsImageValid = false; } @@ -404,7 +583,7 @@ public final class ImageReader implements AutoCloseable { mPlanes[i] = nativeCreatePlane(i); } } - private class SurfacePlane implements android.media.Image.Plane { + private class SurfacePlane extends android.media.Image.Plane { // SurfacePlane instance is created by native code when a new SurfaceImage is created private SurfacePlane(int index, int rowStride, int pixelStride) { mIndex = index; @@ -481,7 +660,7 @@ public final class ImageReader implements AutoCloseable { private synchronized native Surface nativeGetSurface(); private synchronized native boolean nativeImageSetup(Image i); - /* + /** * We use a class initializer to allow the native code to cache some * field offsets. */ diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp index 94f20bc..56ae6b4 100644 --- a/media/jni/android_media_ImageReader.cpp +++ b/media/jni/android_media_ImageReader.cpp @@ -43,8 +43,8 @@ using namespace android; -static const char* const OutOfResourcesException = - "android/view/Surface$OutOfResourcesException"; +static const char* const MaxImagesAcquiredException = + "android/media/ImageReader$MaxImagesAcquiredException"; enum { IMAGE_READER_MAX_NUM_PLANES = 3, @@ -700,7 +700,7 @@ static jboolean ImageReader_imageSetup(JNIEnv* env, jobject thiz, if (buffer == NULL) { ALOGW("Unable to acquire a lockedBuffer, very likely client tries to lock more than" " maxImages buffers"); - jniThrowException(env, OutOfResourcesException, + jniThrowException(env, MaxImagesAcquiredException, "Too many outstanding images, close existing images" " to be able to acquire more."); return false; @@ -709,7 +709,7 @@ static jboolean ImageReader_imageSetup(JNIEnv* env, jobject thiz, if (res != NO_ERROR) { if (res != BAD_VALUE /*no buffers*/) { if (res == NOT_ENOUGH_DATA) { - jniThrowException(env, OutOfResourcesException, + jniThrowException(env, MaxImagesAcquiredException, "Too many outstanding images, close existing images" " to be able to acquire more."); } else { diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java index ecdc287..64b12b7 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaFrameworkUnitTestRunner.java @@ -49,6 +49,7 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner { addMediaPlayerStateUnitTests(suite); addMediaScannerUnitTests(suite); addCameraUnitTests(suite); + addImageReaderTests(suite); return suite; } @@ -65,6 +66,10 @@ public class MediaFrameworkUnitTestRunner extends InstrumentationTestRunner { suite.addTestSuite(CameraMetadataTest.class); } + private void addImageReaderTests(TestSuite suite) { + suite.addTestSuite(ImageReaderTest.class); + } + // Running all unit tests checking the state machine may be time-consuming. private void addMediaMetadataRetrieverStateUnitTests(TestSuite suite) { suite.addTestSuite(MediaMetadataRetrieverTest.class); diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ImageReaderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ImageReaderTest.java new file mode 100644 index 0000000..900fff4 --- /dev/null +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/ImageReaderTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.mediaframeworktest.unit; + +import static org.mockito.Mockito.*; + +import android.graphics.ImageFormat; +import android.media.Image; +import android.media.Image.Plane; +import android.media.ImageReader; +import android.media.ImageReader.OnImageAvailableListener; +import android.media.ImageReader.MaxImagesAcquiredException; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class ImageReaderTest extends AndroidTestCase { + + private static final String TAG = "ImageReaderTest-unit"; + + private static final int DEFAULT_WIDTH = 640; + private static final int DEFAULT_HEIGHT = 480; + private static final int DEFAULT_FORMAT = ImageFormat.YUV_420_888; + private static final int DEFAULT_MAX_IMAGES = 3; + + private ImageReader mReader; + private Image mImage1; + private Image mImage2; + private Image mImage3; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + /** + * Workaround for mockito and JB-MR2 incompatibility + * + * Avoid java.lang.IllegalArgumentException: dexcache == null + * https://code.google.com/p/dexmaker/issues/detail?id=2 + */ + System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString()); + + // TODO: refactor above into one of the test runners + + mReader = spy(ImageReader.newInstance(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_FORMAT, + DEFAULT_MAX_IMAGES)); + mImage1 = mock(Image.class); + mImage2 = mock(Image.class); + mImage3 = mock(Image.class); + + /** + * Ensure rest of classes are mockable + */ + { + mock(Plane.class); + mock(OnImageAvailableListener.class); + mock(MaxImagesAcquiredException.class); + } + + } + + @Override + protected void tearDown() throws Exception { + mReader.close(); + + super.tearDown(); + } + + /** + * Return null when there is nothing in the image queue. + */ + @SmallTest + public void testGetLatestImageEmpty() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(null); + assertEquals(null, mReader.acquireLatestImage()); + } + + /** + * Return the last image from the image queue, close up the rest. + */ + @SmallTest + public void testGetLatestImage1() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(null); + assertEquals(mImage1, mReader.acquireLatestImage()); + verify(mImage1, never()).close(); + } + + /** + * Return the last image from the image queue, close up the rest. + */ + @SmallTest + public void testGetLatestImage2() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(mImage2).thenReturn(null); + assertEquals(mImage2, mReader.acquireLatestImage()); + verify(mImage1, atLeastOnce()).close(); + verify(mImage2, never()).close(); + } + + /** + * Return the last image from the image queue, close up the rest. + */ + @SmallTest + public void testGetLatestImage3() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(mImage2). + thenReturn(mImage3). + thenReturn(null); + assertEquals(mImage3, mReader.acquireLatestImage()); + verify(mImage1, atLeastOnce()).close(); + verify(mImage2, atLeastOnce()).close(); + verify(mImage3, never()).close(); + } + + /** + * Return null if get a MaxImagesAcquiredException with no images in the queue. + */ + @SmallTest + public void testGetLatestImageTooManyBuffersAcquiredEmpty() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenThrow(new MaxImagesAcquiredException()); + try { + mReader.acquireLatestImage(); + fail("Expected MaxImagesAcquiredException to be thrown"); + } catch(MaxImagesAcquiredException e) { + } + } + + /** + * Return the last image before we get a MaxImagesAcquiredException. Close up the rest. + */ + @SmallTest + public void testGetLatestImageTooManyBuffersAcquired1() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1). + thenThrow(new MaxImagesAcquiredException()); + assertEquals(mImage1, mReader.acquireLatestImage()); + verify(mImage1, never()).close(); + } + + /** + * Return the last image before we get a MaxImagesAcquiredException. Close up the rest. + */ + @SmallTest + public void testGetLatestImageTooManyBuffersAcquired2() throws MaxImagesAcquiredException { + + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(mImage2). + thenThrow(new MaxImagesAcquiredException()); + assertEquals(mImage2, mReader.acquireLatestImage()); + verify(mImage1, atLeastOnce()).close(); + verify(mImage2, never()).close(); + } + + /** + * Return the last image before we get a MaxImagesAcquiredException. Close up the rest. + */ + @SmallTest + public void testGetLatestImageTooManyBuffersAcquired3() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(mImage2). + thenReturn(mImage3). + thenThrow(new MaxImagesAcquiredException()); + assertEquals(mImage3, mReader.acquireLatestImage()); + verify(mImage1, atLeastOnce()).close(); + verify(mImage2, atLeastOnce()).close(); + verify(mImage3, never()).close(); + } + + /** + * All images are cleaned up when we get an unexpected Error. + */ + @SmallTest + public void testGetLatestImageExceptionalError() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(mImage2). + thenReturn(mImage3). + thenThrow(new OutOfMemoryError()); + try { + mReader.acquireLatestImage(); + fail("Impossible"); + } catch(OutOfMemoryError e) { + } + + verify(mImage1, atLeastOnce()).close(); + verify(mImage2, atLeastOnce()).close(); + verify(mImage3, atLeastOnce()).close(); + } + + /** + * All images are cleaned up when we get an unexpected RuntimeException. + */ + @SmallTest + public void testGetLatestImageExceptionalRuntime() throws MaxImagesAcquiredException { + when(mReader.acquireNextImage()).thenReturn(mImage1).thenReturn(mImage2). + thenReturn(mImage3). + thenThrow(new RuntimeException()); + try { + mReader.acquireLatestImage(); + fail("Impossible"); + } catch(RuntimeException e) { + } + + verify(mImage1, atLeastOnce()).close(); + verify(mImage2, atLeastOnce()).close(); + verify(mImage3, atLeastOnce()).close(); + } +} |