blob: cc28b8608d847d8163cef8bf424be087ede3aaaa [file] [log] [blame]
/*
* Copyright 2014 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 android.media.cts;
import com.android.cts.media.R;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.cts.util.MediaUtils;
import android.graphics.Rect;
import android.graphics.ImageFormat;
import android.media.cts.CodecUtils;
import android.media.Image;
import android.media.Image.Plane;
import android.media.ImageReader;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.VideoCapabilities;
import android.media.MediaCodecList;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.test.AndroidTestCase;
import android.util.Log;
import android.view.Surface;
import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Basic test for ImageReader APIs.
* <p>
* It uses MediaCodec to decode a short video stream, send the video frames to
* the surface provided by ImageReader. Then compare if output buffers of the
* ImageReader matches the output buffers of the MediaCodec. The video format
* used here is AVC although the compression format doesn't matter for this
* test. For decoder test, hw and sw decoders are tested,
* </p>
*/
public class ImageReaderDecoderTest extends AndroidTestCase {
private static final String TAG = "ImageReaderDecoderTest";
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final long DEFAULT_TIMEOUT_US = 10000;
private static final long WAIT_FOR_IMAGE_TIMEOUT_MS = 1000;
private static final String DEBUG_FILE_NAME_BASE = "/sdcard/";
private static final int NUM_FRAME_DECODED = 100;
// video decoders only support a single outstanding image with the consumer
private static final int MAX_NUM_IMAGES = 1;
private static final float COLOR_STDEV_ALLOWANCE = 5f;
private static final float COLOR_DELTA_ALLOWANCE = 5f;
private final static int MODE_IMAGEREADER = 0;
private final static int MODE_IMAGE = 1;
private Resources mResources;
private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
private ImageReader mReader;
private Surface mReaderSurface;
private HandlerThread mHandlerThread;
private Handler mHandler;
private ImageListener mImageListener;
@Override
public void setContext(Context context) {
super.setContext(context);
mResources = mContext.getResources();
}
@Override
protected void setUp() throws Exception {
super.setUp();
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mImageListener = new ImageListener();
}
@Override
protected void tearDown() throws Exception {
mHandlerThread.quitSafely();
mHandler = null;
}
static class MediaAsset {
public MediaAsset(int resource, int width, int height) {
mResource = resource;
mWidth = width;
mHeight = height;
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
public int getResource() {
return mResource;
}
private final int mResource;
private final int mWidth;
private final int mHeight;
}
static class MediaAssets {
public MediaAssets(String mime, MediaAsset... assets) {
mMime = mime;
mAssets = assets;
}
public String getMime() {
return mMime;
}
public MediaAsset[] getAssets() {
return mAssets;
}
private final String mMime;
private final MediaAsset[] mAssets;
}
private static MediaAssets H263_ASSETS = new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_H263,
new MediaAsset(R.raw.swirl_176x144_h263, 176, 144),
new MediaAsset(R.raw.swirl_352x288_h263, 352, 288),
new MediaAsset(R.raw.swirl_128x96_h263, 128, 96));
private static MediaAssets MPEG4_ASSETS = new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_MPEG4,
new MediaAsset(R.raw.swirl_128x128_mpeg4, 128, 128),
new MediaAsset(R.raw.swirl_144x136_mpeg4, 144, 136),
new MediaAsset(R.raw.swirl_136x144_mpeg4, 136, 144),
new MediaAsset(R.raw.swirl_132x130_mpeg4, 132, 130),
new MediaAsset(R.raw.swirl_130x132_mpeg4, 130, 132));
private static MediaAssets H264_ASSETS = new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_AVC,
new MediaAsset(R.raw.swirl_128x128_h264, 128, 128),
new MediaAsset(R.raw.swirl_144x136_h264, 144, 136),
new MediaAsset(R.raw.swirl_136x144_h264, 136, 144),
new MediaAsset(R.raw.swirl_132x130_h264, 132, 130),
new MediaAsset(R.raw.swirl_130x132_h264, 130, 132));
private static MediaAssets H265_ASSETS = new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_HEVC,
new MediaAsset(R.raw.swirl_128x128_h265, 128, 128),
new MediaAsset(R.raw.swirl_144x136_h265, 144, 136),
new MediaAsset(R.raw.swirl_136x144_h265, 136, 144),
new MediaAsset(R.raw.swirl_132x130_h265, 132, 130),
new MediaAsset(R.raw.swirl_130x132_h265, 130, 132));
private static MediaAssets VP8_ASSETS = new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_VP8,
new MediaAsset(R.raw.swirl_128x128_vp8, 128, 128),
new MediaAsset(R.raw.swirl_144x136_vp8, 144, 136),
new MediaAsset(R.raw.swirl_136x144_vp8, 136, 144),
new MediaAsset(R.raw.swirl_132x130_vp8, 132, 130),
new MediaAsset(R.raw.swirl_130x132_vp8, 130, 132));
private static MediaAssets VP9_ASSETS = new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_VP9,
new MediaAsset(R.raw.swirl_128x128_vp9, 128, 128),
new MediaAsset(R.raw.swirl_144x136_vp9, 144, 136),
new MediaAsset(R.raw.swirl_136x144_vp9, 136, 144),
new MediaAsset(R.raw.swirl_132x130_vp9, 132, 130),
new MediaAsset(R.raw.swirl_130x132_vp9, 130, 132));
static final float SWIRL_FPS = 12.f;
class Decoder {
final private String mName;
final private String mMime;
final private VideoCapabilities mCaps;
final private ArrayList<MediaAsset> mAssets;
boolean isFlexibleFormatSupported(CodecCapabilities caps) {
for (int c : caps.colorFormats) {
if (c == COLOR_FormatYUV420Flexible) {
return true;
}
}
return false;
}
Decoder(String name, MediaAssets assets, CodecCapabilities caps) {
mName = name;
mMime = assets.getMime();
mCaps = caps.getVideoCapabilities();
mAssets = new ArrayList<MediaAsset>();
for (MediaAsset asset : assets.getAssets()) {
if (mCaps.areSizeAndRateSupported(asset.getWidth(), asset.getHeight(), SWIRL_FPS)
&& isFlexibleFormatSupported(caps)) {
mAssets.add(asset);
}
}
}
public boolean videoDecode(int mode, boolean checkSwirl) {
boolean skipped = true;
for (MediaAsset asset: mAssets) {
// TODO: loop over all supported image formats
int imageFormat = ImageFormat.YUV_420_888;
int colorFormat = COLOR_FormatYUV420Flexible;
videoDecode(asset, imageFormat, colorFormat, mode, checkSwirl);
skipped = false;
}
return skipped;
}
private void videoDecode(
MediaAsset asset, int imageFormat, int colorFormat, int mode, boolean checkSwirl) {
int video = asset.getResource();
int width = asset.getWidth();
int height = asset.getHeight();
if (DEBUG) Log.d(TAG, "videoDecode " + mName + " " + width + "x" + height);
MediaCodec decoder = null;
AssetFileDescriptor vidFD = null;
MediaExtractor extractor = null;
File tmpFile = null;
InputStream is = null;
FileOutputStream os = null;
MediaFormat mediaFormat = null;
try {
extractor = new MediaExtractor();
try {
vidFD = mResources.openRawResourceFd(video);
extractor.setDataSource(
vidFD.getFileDescriptor(), vidFD.getStartOffset(), vidFD.getLength());
} catch (NotFoundException e) {
// resource is compressed, uncompress locally
String tmpName = "tempStream";
tmpFile = File.createTempFile(tmpName, null, mContext.getCacheDir());
is = mResources.openRawResource(video);
os = new FileOutputStream(tmpFile);
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf, 0, buf.length)) > 0) {
os.write(buf, 0, len);
}
os.close();
is.close();
extractor.setDataSource(tmpFile.getAbsolutePath());
}
mediaFormat = extractor.getTrackFormat(0);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
// Create decoder
decoder = MediaCodec.createByCodecName(mName);
assertNotNull("couldn't create decoder" + mName, decoder);
decodeFramesToImage(
decoder, extractor, mediaFormat,
width, height, imageFormat, mode, checkSwirl);
decoder.stop();
if (vidFD != null) {
vidFD.close();
}
} catch (Throwable e) {
throw new RuntimeException("while " + mName + " decoding "
+ mResources.getResourceEntryName(video) + ": " + mediaFormat, e);
} finally {
if (decoder != null) {
decoder.release();
}
if (extractor != null) {
extractor.release();
}
if (tmpFile != null) {
tmpFile.delete();
}
}
}
}
private Decoder[] decoders(MediaAssets assets, boolean goog) {
String mime = assets.getMime();
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
ArrayList<Decoder> result = new ArrayList<Decoder>();
for (MediaCodecInfo info : mcl.getCodecInfos()) {
if (info.isEncoder()
|| info.getName().toLowerCase().startsWith("omx.google.") != goog) {
continue;
}
CodecCapabilities caps = null;
try {
caps = info.getCapabilitiesForType(mime);
} catch (IllegalArgumentException e) { // mime is not supported
continue;
}
assertNotNull(info.getName() + " capabilties for " + mime + " returned null", caps);
result.add(new Decoder(info.getName(), assets, caps));
}
return result.toArray(new Decoder[result.size()]);
}
private Decoder[] goog(MediaAssets assets) {
return decoders(assets, true /* goog */);
}
private Decoder[] other(MediaAssets assets) {
return decoders(assets, false /* goog */);
}
private Decoder[] googH265() { return goog(H265_ASSETS); }
private Decoder[] googH264() { return goog(H264_ASSETS); }
private Decoder[] googH263() { return goog(H263_ASSETS); }
private Decoder[] googMpeg4() { return goog(MPEG4_ASSETS); }
private Decoder[] googVP8() { return goog(VP8_ASSETS); }
private Decoder[] googVP9() { return goog(VP9_ASSETS); }
private Decoder[] otherH265() { return other(H265_ASSETS); }
private Decoder[] otherH264() { return other(H264_ASSETS); }
private Decoder[] otherH263() { return other(H263_ASSETS); }
private Decoder[] otherMpeg4() { return other(MPEG4_ASSETS); }
private Decoder[] otherVP8() { return other(VP8_ASSETS); }
private Decoder[] otherVP9() { return other(VP9_ASSETS); }
public void testGoogH265Image() { swirlTest(googH265(), MODE_IMAGE); }
public void testGoogH264Image() { swirlTest(googH264(), MODE_IMAGE); }
public void testGoogH263Image() { swirlTest(googH263(), MODE_IMAGE); }
public void testGoogMpeg4Image() { swirlTest(googMpeg4(), MODE_IMAGE); }
public void testGoogVP8Image() { swirlTest(googVP8(), MODE_IMAGE); }
public void testGoogVP9Image() { swirlTest(googVP9(), MODE_IMAGE); }
public void testOtherH265Image() { swirlTest(otherH265(), MODE_IMAGE); }
public void testOtherH264Image() { swirlTest(otherH264(), MODE_IMAGE); }
public void testOtherH263Image() { swirlTest(otherH263(), MODE_IMAGE); }
public void testOtherMpeg4Image() { swirlTest(otherMpeg4(), MODE_IMAGE); }
public void testOtherVP8Image() { swirlTest(otherVP8(), MODE_IMAGE); }
public void testOtherVP9Image() { swirlTest(otherVP9(), MODE_IMAGE); }
public void testGoogH265ImageReader() { swirlTest(googH265(), MODE_IMAGEREADER); }
public void testGoogH264ImageReader() { swirlTest(googH264(), MODE_IMAGEREADER); }
public void testGoogH263ImageReader() { swirlTest(googH263(), MODE_IMAGEREADER); }
public void testGoogMpeg4ImageReader() { swirlTest(googMpeg4(), MODE_IMAGEREADER); }
public void testGoogVP8ImageReader() { swirlTest(googVP8(), MODE_IMAGEREADER); }
public void testGoogVP9ImageReader() { swirlTest(googVP9(), MODE_IMAGEREADER); }
public void testOtherH265ImageReader() { swirlTest(otherH265(), MODE_IMAGEREADER); }
public void testOtherH264ImageReader() { swirlTest(otherH264(), MODE_IMAGEREADER); }
public void testOtherH263ImageReader() { swirlTest(otherH263(), MODE_IMAGEREADER); }
public void testOtherMpeg4ImageReader() { swirlTest(otherMpeg4(), MODE_IMAGEREADER); }
public void testOtherVP8ImageReader() { swirlTest(otherVP8(), MODE_IMAGEREADER); }
public void testOtherVP9ImageReader() { swirlTest(otherVP9(), MODE_IMAGEREADER); }
/**
* Test ImageReader with 480x360 non-google AVC decoding for flexible yuv format
*/
public void testHwAVCDecode360pForFlexibleYuv() throws Exception {
Decoder[] decoders = other(new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_AVC,
new MediaAsset(
R.raw.video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz,
480 /* width */, 360 /* height */)));
decodeTest(decoders, MODE_IMAGEREADER, false /* checkSwirl */);
}
/**
* Test ImageReader with 480x360 google (SW) AVC decoding for flexible yuv format
*/
public void testSwAVCDecode360pForFlexibleYuv() throws Exception {
Decoder[] decoders = goog(new MediaAssets(
MediaFormat.MIMETYPE_VIDEO_AVC,
new MediaAsset(
R.raw.video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz,
480 /* width */, 360 /* height */)));
decodeTest(decoders, MODE_IMAGEREADER, false /* checkSwirl */);
}
private void swirlTest(Decoder[] decoders, int mode) {
decodeTest(decoders, mode, true /* checkSwirl */);
}
private void decodeTest(Decoder[] decoders, int mode, boolean checkSwirl) {
try {
boolean skipped = true;
for (Decoder codec : decoders) {
if (codec.videoDecode(mode, checkSwirl)) {
skipped = false;
}
}
if (skipped) {
MediaUtils.skipTest("decoder does not any of the input files");
}
} finally {
closeImageReader();
}
}
private static class ImageListener implements ImageReader.OnImageAvailableListener {
private final LinkedBlockingQueue<Image> mQueue =
new LinkedBlockingQueue<Image>();
@Override
public void onImageAvailable(ImageReader reader) {
try {
mQueue.put(reader.acquireNextImage());
} catch (InterruptedException e) {
throw new UnsupportedOperationException(
"Can't handle InterruptedException in onImageAvailable");
}
}
/**
* Get an image from the image reader.
*
* @param timeout Timeout value for the wait.
* @return The image from the image reader.
*/
public Image getImage(long timeout) throws InterruptedException {
Image image = mQueue.poll(timeout, TimeUnit.MILLISECONDS);
assertNotNull("Wait for an image timed out in " + timeout + "ms", image);
return image;
}
}
/**
* Decode video frames to image reader.
*/
private void decodeFramesToImage(
MediaCodec decoder, MediaExtractor extractor, MediaFormat mediaFormat,
int width, int height, int imageFormat, int mode, boolean checkSwirl)
throws InterruptedException {
ByteBuffer[] decoderInputBuffers;
ByteBuffer[] decoderOutputBuffers;
// Configure decoder.
if (VERBOSE) Log.v(TAG, "stream format: " + mediaFormat);
if (mode == MODE_IMAGEREADER) {
createImageReader(width, height, imageFormat, MAX_NUM_IMAGES, mImageListener);
decoder.configure(mediaFormat, mReaderSurface, null /* crypto */, 0 /* flags */);
} else {
assertEquals(mode, MODE_IMAGE);
decoder.configure(mediaFormat, null /* surface */, null /* crypto */, 0 /* flags */);
}
decoder.start();
decoderInputBuffers = decoder.getInputBuffers();
decoderOutputBuffers = decoder.getOutputBuffers();
extractor.selectTrack(0);
// Start decoding and get Image, only test the first NUM_FRAME_DECODED frames.
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int outputFrameCount = 0;
while (!sawOutputEOS && outputFrameCount < NUM_FRAME_DECODED) {
if (VERBOSE) Log.v(TAG, "loop:" + outputFrameCount);
// Feed input frame.
if (!sawInputEOS) {
int inputBufIndex = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = decoderInputBuffers[inputBufIndex];
int sampleSize =
extractor.readSampleData(dstBuf, 0 /* offset */);
if (VERBOSE) Log.v(TAG, "queue a input buffer, idx/size: "
+ inputBufIndex + "/" + sampleSize);
long presentationTimeUs = 0;
if (sampleSize < 0) {
if (VERBOSE) Log.v(TAG, "saw input EOS.");
sawInputEOS = true;
sampleSize = 0;
} else {
presentationTimeUs = extractor.getSampleTime();
}
decoder.queueInputBuffer(
inputBufIndex,
0 /* offset */,
sampleSize,
presentationTimeUs,
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
if (!sawInputEOS) {
extractor.advance();
}
}
}
// Get output frame
int res = decoder.dequeueOutputBuffer(info, DEFAULT_TIMEOUT_US);
if (VERBOSE) Log.v(TAG, "got a buffer: " + info.size + "/" + res);
if (res == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) Log.v(TAG, "no output frame available");
} else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// decoder output buffers changed, need update.
if (VERBOSE) Log.v(TAG, "decoder output buffers changed");
decoderOutputBuffers = decoder.getOutputBuffers();
} else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// this happens before the first frame is returned.
MediaFormat outFormat = decoder.getOutputFormat();
if (VERBOSE) Log.v(TAG, "decoder output format changed: " + outFormat);
} else if (res < 0) {
// Should be decoding error.
fail("unexpected result from deocder.dequeueOutputBuffer: " + res);
} else {
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
sawOutputEOS = true;
}
// res >= 0: normal decoding case, copy the output buffer.
// Will use it as reference to valid the ImageReader output
// Some decoders output a 0-sized buffer at the end. Ignore those.
boolean doRender = (info.size != 0);
if (doRender) {
outputFrameCount++;
String fileName = DEBUG_FILE_NAME_BASE + MediaUtils.getTestName()
+ (mode == MODE_IMAGE ? "_image_" : "_reader_")
+ width + "x" + height + "_" + outputFrameCount + ".yuv";
Image image = null;
try {
if (mode == MODE_IMAGE) {
image = decoder.getOutputImage(res);
} else {
decoder.releaseOutputBuffer(res, doRender);
res = -1;
// Read image and verify
image = mImageListener.getImage(WAIT_FOR_IMAGE_TIMEOUT_MS);
}
validateImage(image, width, height, imageFormat, fileName);
if (checkSwirl) {
try {
validateSwirl(image);
} catch (Throwable e) {
dumpFile(fileName, getDataFromImage(image));
throw e;
}
}
} finally {
if (image != null) {
image.close();
}
}
}
if (res >= 0) {
decoder.releaseOutputBuffer(res, false /* render */);
}
}
}
}
/**
* Validate image based on format and size.
*
* @param image The image to be validated.
* @param width The image width.
* @param height The image height.
* @param format The image format.
* @param filePath The debug dump file path, null if don't want to dump to file.
*/
public static void validateImage(
Image image, int width, int height, int format, String filePath) {
if (VERBOSE) {
Plane[] imagePlanes = image.getPlanes();
Log.v(TAG, "Image " + filePath + " Info:");
Log.v(TAG, "first plane pixelstride " + imagePlanes[0].getPixelStride());
Log.v(TAG, "first plane rowstride " + imagePlanes[0].getRowStride());
Log.v(TAG, "Image timestamp:" + image.getTimestamp());
}
assertNotNull("Input image is invalid", image);
assertEquals("Format doesn't match", format, image.getFormat());
assertEquals("Width doesn't match", width, image.getCropRect().width());
assertEquals("Height doesn't match", height, image.getCropRect().height());
if(VERBOSE) Log.v(TAG, "validating Image");
byte[] data = getDataFromImage(image);
assertTrue("Invalid image data", data != null && data.length > 0);
validateYuvData(data, width, height, format, image.getTimestamp());
if (VERBOSE && filePath != null) {
dumpFile(filePath, data);
}
}
private static void validateSwirl(Image image) {
Rect crop = image.getCropRect();
final int NUM_SIDES = 4;
final int step = 8; // the width of the layers
long[][] rawStats = new long[NUM_SIDES][10];
int[][] colors = new int[][] {
{ 111, 96, 204 }, { 178, 27, 174 }, { 100, 192, 92 }, { 106, 117, 62 }
};
// successively accumulate statistics for each layer of the swirl
// by using overlapping rectangles, and the observation that
// layer_i = rectangle_i - rectangle_(i+1)
int lastLayer = 0;
int layer = 0;
boolean lastLayerValid = false;
for (int pos = 0; ; pos += step) {
Rect area = new Rect(pos - step, pos, crop.width() / 2, crop.height() + 2 * step - pos);
if (area.isEmpty()) {
break;
}
area.offset(crop.left, crop.top);
area.intersect(crop);
for (int lr = 0; lr < 2; ++lr) {
long[] oneStat = CodecUtils.getRawStats(image, area);
if (VERBOSE) Log.v(TAG, "area=" + area + ", layer=" + layer + ", last="
+ lastLayer + ": " + Arrays.toString(oneStat));
for (int i = 0; i < oneStat.length; i++) {
rawStats[layer][i] += oneStat[i];
if (lastLayerValid) {
rawStats[lastLayer][i] -= oneStat[i];
}
}
if (VERBOSE && lastLayerValid) {
Log.v(TAG, "layer-" + lastLayer + ": " + Arrays.toString(rawStats[lastLayer]));
Log.v(TAG, Arrays.toString(CodecUtils.Raw2YUVStats(rawStats[lastLayer])));
}
// switch to the opposite side
layer ^= 2; // NUM_SIDES / 2
lastLayer ^= 2; // NUM_SIDES / 2
area.offset(crop.centerX() - area.left, 2 * (crop.centerY() - area.centerY()));
}
lastLayer = layer;
lastLayerValid = true;
layer = (layer + 1) % NUM_SIDES;
}
for (layer = 0; layer < NUM_SIDES; ++layer) {
float[] stats = CodecUtils.Raw2YUVStats(rawStats[layer]);
if (DEBUG) Log.d(TAG, "layer-" + layer + ": " + Arrays.toString(stats));
if (VERBOSE) Log.v(TAG, Arrays.toString(rawStats[layer]));
// check layer uniformity
for (int i = 0; i < 3; i++) {
assertTrue("color of layer-" + layer + " is not uniform: "
+ Arrays.toString(stats),
stats[3 + i] < COLOR_STDEV_ALLOWANCE);
}
// check layer color
for (int i = 0; i < 3; i++) {
assertTrue("color of layer-" + layer + " mismatches target "
+ Arrays.toString(colors[layer]) + " vs "
+ Arrays.toString(Arrays.copyOf(stats, 3)),
Math.abs(stats[i] - colors[layer][i]) < COLOR_DELTA_ALLOWANCE);
}
}
}
private static void validateYuvData(byte[] yuvData, int width, int height, int format,
long ts) {
assertTrue("YUV format must be one of the YUV_420_888, NV21, or YV12",
format == ImageFormat.YUV_420_888 ||
format == ImageFormat.NV21 ||
format == ImageFormat.YV12);
if (VERBOSE) Log.v(TAG, "Validating YUV data");
int expectedSize = width * height * ImageFormat.getBitsPerPixel(format) / 8;
assertEquals("Yuv data doesn't match", expectedSize, yuvData.length);
}
private static void checkYuvFormat(int format) {
if ((format != ImageFormat.YUV_420_888) &&
(format != ImageFormat.NV21) &&
(format != ImageFormat.YV12)) {
fail("Wrong formats: " + format);
}
}
/**
* <p>Check android image format validity for an image, only support below formats:</p>
*
* <p>Valid formats are YUV_420_888/NV21/YV12 for video decoder</p>
*/
private static void checkAndroidImageFormat(Image image) {
int format = image.getFormat();
Plane[] planes = image.getPlanes();
switch (format) {
case ImageFormat.YUV_420_888:
case ImageFormat.NV21:
case ImageFormat.YV12:
assertEquals("YUV420 format Images should have 3 planes", 3, planes.length);
break;
default:
fail("Unsupported Image Format: " + format);
}
}
/**
* Get a byte array image data from an Image object.
* <p>
* Read data from all planes of an Image into a contiguous unpadded,
* unpacked 1-D linear byte array, such that it can be write into disk, or
* accessed by software conveniently. It supports YUV_420_888/NV21/YV12
* input Image format.
* </p>
* <p>
* For YUV_420_888/NV21/YV12/Y8/Y16, it returns a byte array that contains
* the Y plane data first, followed by U(Cb), V(Cr) planes if there is any
* (xstride = width, ystride = height for chroma and luma components).
* </p>
*/
private static byte[] getDataFromImage(Image image) {
assertNotNull("Invalid image:", image);
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
int rowStride, pixelStride;
byte[] data = null;
// Read image data
Plane[] planes = image.getPlanes();
assertTrue("Fail to get image planes", planes != null && planes.length > 0);
// Check image validity
checkAndroidImageFormat(image);
ByteBuffer buffer = null;
int offset = 0;
data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
if(VERBOSE) Log.v(TAG, "get data from " + planes.length + " planes");
for (int i = 0; i < planes.length; i++) {
int shift = (i == 0) ? 0 : 1;
buffer = planes[i].getBuffer();
assertNotNull("Fail to get bytebuffer from plane", buffer);
rowStride = planes[i].getRowStride();
pixelStride = planes[i].getPixelStride();
assertTrue("pixel stride " + pixelStride + " is invalid", pixelStride > 0);
if (VERBOSE) {
Log.v(TAG, "pixelStride " + pixelStride);
Log.v(TAG, "rowStride " + rowStride);
Log.v(TAG, "width " + width);
Log.v(TAG, "height " + height);
}
// For multi-planar yuv images, assuming yuv420 with 2x2 chroma subsampling.
int w = crop.width() >> shift;
int h = crop.height() >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
assertTrue("rowStride " + rowStride + " should be >= width " + w , rowStride >= w);
for (int row = 0; row < h; row++) {
int bytesPerPixel = ImageFormat.getBitsPerPixel(format) / 8;
int length;
if (pixelStride == bytesPerPixel) {
// Special case: optimized read of the entire row
length = w * bytesPerPixel;
buffer.get(data, offset, length);
offset += length;
} else {
// Generic case: should work for any pixelStride but slower.
// Use intermediate buffer to avoid read byte-by-byte from
// DirectByteBuffer, which is very bad for performance
length = (w - 1) * pixelStride + bytesPerPixel;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++) {
data[offset++] = rowData[col * pixelStride];
}
}
// Advance buffer the remainder of the row stride
if (row < h - 1) {
buffer.position(buffer.position() + rowStride - length);
}
}
if (VERBOSE) Log.v(TAG, "Finished reading data from plane " + i);
}
return data;
}
private static void dumpFile(String fileName, byte[] data) {
assertNotNull("fileName must not be null", fileName);
assertNotNull("data must not be null", data);
FileOutputStream outStream;
try {
Log.v(TAG, "output will be saved as " + fileName);
outStream = new FileOutputStream(fileName);
} catch (IOException ioe) {
throw new RuntimeException("Unable to create debug output file " + fileName, ioe);
}
try {
outStream.write(data);
outStream.close();
} catch (IOException ioe) {
throw new RuntimeException("failed writing data to file " + fileName, ioe);
}
}
private void createImageReader(
int width, int height, int format, int maxNumImages,
ImageReader.OnImageAvailableListener listener) {
closeImageReader();
mReader = ImageReader.newInstance(width, height, format, maxNumImages);
mReaderSurface = mReader.getSurface();
mReader.setOnImageAvailableListener(listener, mHandler);
if (VERBOSE) {
Log.v(TAG, String.format("Created ImageReader size (%dx%d), format %d", width, height,
format));
}
}
/**
* Close the pending images then close current active {@link ImageReader} object.
*/
private void closeImageReader() {
if (mReader != null) {
try {
// Close all possible pending images first.
Image image = mReader.acquireLatestImage();
if (image != null) {
image.close();
}
} finally {
mReader.close();
mReader = null;
}
}
}
}