Basic verification tests for VPX encoder.

A basic confirmation about a frame being properly
encoded is done by trying to decode it.
(Since in realtime mode vp8 encode output is non-
deterministic, a md5 sum comparison wouldn't work.)

A raw yv12 stream is encoded and written to
an IVF file, which is later decoded by vp8 decoder to
verify frames are at least decodable.

Associated raw stream and IVF reader+writer is included.

Change-Id: Ib672794ce5dca3812b5b16d54f74d4590502346b
Signed-off-by: Kunter Gultekin <kuntergultekin@google.com>
diff --git a/tests/tests/media/res/raw/video_176x144_yv12.raw b/tests/tests/media/res/raw/video_176x144_yv12.raw
new file mode 100644
index 0000000..23af164
--- /dev/null
+++ b/tests/tests/media/res/raw/video_176x144_yv12.raw
Binary files differ
diff --git a/tests/tests/media/src/android/media/cts/IvfReader.java b/tests/tests/media/src/android/media/cts/IvfReader.java
new file mode 100644
index 0000000..508ae25
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/IvfReader.java
@@ -0,0 +1,193 @@
+/*
+ * 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 android.media.cts;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * A simple reader for an IVF file.
+ *
+ * IVF format is a simple container format for VP8 encoded frames.
+ * This reader is capable of getting frame count, width and height
+ * from the header, and access individual frames randomly by
+ * frame number.
+ */
+
+public class IvfReader {
+    private static final byte HEADER_END = 32;
+    private static final byte FOURCC_HEAD = 8;
+    private static final byte WIDTH_HEAD = 12;
+    private static final byte HEIGHT_HEAD = 14;
+    private static final byte FRAMECOUNT_HEAD = 24;
+    private static final byte FRAME_HEADER_SIZE = 12;
+
+    private RandomAccessFile mIvfFile;
+    private boolean mHeaderValid;
+    private int mWidth;
+    private int mHeight;
+    private int mFrameCount;
+    private int[] mFrameHeads;  // Head of frame header
+    private int[] mFrameSizes;  // Frame size excluding header
+
+
+    /**
+     * Initializes the IVF file reader.
+     *
+     * Only minimal verification is done to check if this
+     * is indeed a valid IVF file. (fourcc, signature)
+     *
+     * All frame headers are read in advance.
+     *
+     * @param filename   name of the IVF file
+     */
+    public IvfReader(String filename) throws IOException{
+        mIvfFile = new RandomAccessFile(filename, "r");
+
+        mHeaderValid = verifyHeader();
+        readHeaderData();
+        readFrameMetadata();
+    }
+
+    /**
+     * Tells if file header seems to be valid.
+     *
+     * Only minimal verification is done to check if this
+     * is indeed a valid IVF file. (fourcc, signature)
+     */
+    public boolean isHeaderValid(){
+        return mHeaderValid;
+    }
+
+    /**
+     * Returns frame width according to header information.
+     */
+    public int getWidth(){
+        return mWidth;
+    }
+
+    /**
+     * Returns frame height according to header information.
+     */
+    public int getHeight(){
+        return mHeight;
+    }
+
+    /**
+     * Returns frame count according to header information.
+     */
+    public int getFrameCount(){
+        return mFrameCount;
+    }
+
+    /**
+     * Returns frame data by index.
+     *
+     * @param frameIndex index of the frame to read, greater-equal
+     * than 0 and less than frameCount.
+     */
+    public byte[] readFrame(int frameIndex) throws IOException {
+        if (frameIndex > mFrameCount | frameIndex < 0){
+            return null;
+        }
+        int frameSize = mFrameSizes[frameIndex];
+        int frameHead = mFrameHeads[frameIndex];
+
+        byte[] frame = new byte[frameSize];
+        mIvfFile.seek(frameHead + FRAME_HEADER_SIZE);
+        mIvfFile.read(frame);
+
+        return frame;
+    }
+
+    /**
+     * Closes IVF file.
+     */
+    public void close() throws IOException{
+        mIvfFile.close();
+    }
+
+    private boolean verifyHeader() throws IOException{
+        mIvfFile.seek(0);
+
+        if (mIvfFile.length() < HEADER_END){
+            return false;
+        }
+
+        // DKIF signature
+        boolean signatureMatch = ((mIvfFile.readByte() == (byte)'D') &&
+                (mIvfFile.readByte() == (byte)'K') &&
+                (mIvfFile.readByte() == (byte)'I') &&
+                (mIvfFile.readByte() == (byte)'F'));
+
+        // Fourcc
+        mIvfFile.seek(FOURCC_HEAD);
+        boolean fourccMatch = ((mIvfFile.readByte() == (byte)'V') &&
+                (mIvfFile.readByte() == (byte)'P') &&
+                (mIvfFile.readByte() == (byte)'8') &&
+                (mIvfFile.readByte() == (byte)'0'));
+
+        return signatureMatch && fourccMatch;
+    }
+
+    private void readHeaderData() throws IOException{
+        // width
+        mIvfFile.seek(WIDTH_HEAD);
+        mWidth = (int) changeEndianness(mIvfFile.readShort());
+
+        // height
+        mIvfFile.seek(HEIGHT_HEAD);
+        mHeight = (int) changeEndianness(mIvfFile.readShort());
+
+        // frame count
+        mIvfFile.seek(FRAMECOUNT_HEAD);
+        mFrameCount = changeEndianness(mIvfFile.readInt());
+
+        // allocate frame metadata
+        mFrameHeads = new int[mFrameCount];
+        mFrameSizes = new int[mFrameCount];
+    }
+
+    private void readFrameMetadata() throws IOException{
+        int frameHead = HEADER_END;
+        for(int i = 0; i < mFrameCount; i++){
+            mIvfFile.seek(frameHead);
+            int frameSize = changeEndianness(mIvfFile.readInt());
+            mFrameHeads[i] = frameHead;
+            mFrameSizes[i] = frameSize;
+            // next frame
+            frameHead += FRAME_HEADER_SIZE + frameSize;
+        }
+    }
+
+    private static short changeEndianness(short value){
+        // Rationale for down-cast;
+        // Java Language specification 15.19:
+        //  "The type of the shift expression is the promoted type of the left-hand operand."
+        // Java Language specification 5.6:
+        //  "...if the operand is of compile-time type byte, short, or char,
+        //  unary numeric promotion promotes it to a value of type int by a widening conversion."
+        return (short) (((value << 8) & 0XFF00) | ((value >> 8) & 0X00FF));
+    }
+
+    private static int changeEndianness(int value){
+        return (((value << 24) & 0XFF000000) |
+                ((value << 8)  & 0X00FF0000) |
+                ((value >> 8)  & 0X0000FF00) |
+                ((value >> 24) & 0X000000FF));
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/IvfWriter.java b/tests/tests/media/src/android/media/cts/IvfWriter.java
new file mode 100644
index 0000000..ccc0ac5
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/IvfWriter.java
@@ -0,0 +1,187 @@
+/*
+ * 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 android.media.cts;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * Writes an IVF file.
+ *
+ * IVF format is a simple container format for VP8 encoded frames.
+ */
+
+public class IvfWriter {
+    private static final byte HEADER_END = 32;
+    private RandomAccessFile mOutputFile;
+    private int mWidth;
+    private int mHeight;
+    private int mScale;
+    private int mRate;
+    private int mFrameCount;
+
+    /**
+     * Initializes the IVF file writer.
+     *
+     * Timebase fraction is in format scale/rate, e.g. 1/1000
+     * Timestamp values supplied while writing frames should be in accordance
+     * with this timebase value.
+     *
+     * @param filename   name of the IVF file
+     * @param width      frame width
+     * @param height     frame height
+     * @param scale      timebase scale (or numerator of the timebase fraction)
+     * @param rate       timebase rate (or denominator of the timebase fraction)
+     */
+    public IvfWriter(String filename,
+                     int width, int height,
+                     int scale, int rate) throws IOException {
+        mOutputFile = new RandomAccessFile(filename, "rw");
+        mWidth = width;
+        mHeight = height;
+        mScale = scale;
+        mRate = rate;
+        mFrameCount = 0;
+        mOutputFile.seek(HEADER_END);  // Skip the header for now, as framecount is unknown
+    }
+
+    /**
+     * Initializes the IVF file writer with a microsecond timebase.
+     *
+     *
+     * Microsecond timebase is default for OMX thus stagefright.
+     *
+     * @param filename   name of the IVF file
+     * @param width      frame width
+     * @param height     frame height
+     */
+    public IvfWriter(String filename, int width, int height) throws IOException {
+        this(filename, width, height, 1, 1000000);
+    }
+
+    /**
+     * Finalizes the IVF header and closes the file.
+     */
+    public void close() throws IOException{
+        // Write header now
+        mOutputFile.seek(0);
+        mOutputFile.write(makeIvfHeader(mFrameCount, mWidth, mHeight, mScale, mRate));
+        mOutputFile.close();
+    }
+
+    /**
+     * Writes a single encoded VP8 frame with its frame header.
+     *
+     * @param frame     actual contents of the encoded frame data
+     * @param width     timestamp of the frame (in accordance to specified timebase)
+     */
+    public void writeFrame(byte[] frame, long timeStamp) throws IOException {
+        mOutputFile.write(makeIvfFrameHeader(frame.length, timeStamp));
+        mOutputFile.write(frame);
+        mFrameCount++;
+    }
+
+    /**
+     * Makes a 32 byte file header for IVF format.
+     *
+     * Timebase fraction is in format scale/rate, e.g. 1/1000
+     *
+     * @param frameCount total number of frames file contains
+     * @param width      frame width
+     * @param height     frame height
+     * @param scale      timebase scale (or numerator of the timebase fraction)
+     * @param rate       timebase rate (or denominator of the timebase fraction)
+     */
+    private static byte[] makeIvfHeader(int frameCount, int width, int height, int scale, int rate){
+        byte[] ivfHeader = new byte[32];
+        ivfHeader[0] = 'D';
+        ivfHeader[1] = 'K';
+        ivfHeader[2] = 'I';
+        ivfHeader[3] = 'F';
+        lay16Bits(ivfHeader, 4, 0);  // version
+        lay16Bits(ivfHeader, 6, 32);  // header size
+        ivfHeader[8] = 'V';  // fourcc
+        ivfHeader[9] = 'P';
+        ivfHeader[10] = '8';
+        ivfHeader[11] = '0';
+        lay16Bits(ivfHeader, 12, width);
+        lay16Bits(ivfHeader, 14, height);
+        lay32Bits(ivfHeader, 16, rate);  // scale/rate
+        lay32Bits(ivfHeader, 20, scale);
+        lay32Bits(ivfHeader, 24, frameCount);
+        lay32Bits(ivfHeader, 28, 0);  // unused
+        return ivfHeader;
+    }
+
+    /**
+     * Makes a 12 byte header for an encoded frame.
+     *
+     * @param size      frame size
+     * @param timestamp presentation timestamp of the frame
+     */
+    private static byte[] makeIvfFrameHeader(int size, long timestamp){
+        byte[] frameHeader = new byte[12];
+        lay32Bits(frameHeader, 0, size);
+        lay64bits(frameHeader, 4, timestamp);
+        return frameHeader;
+    }
+
+
+    /**
+     * Lays least significant 16 bits of an int into 2 items of a byte array.
+     *
+     * Note that ordering is little-endian.
+     *
+     * @param array     the array to be modified
+     * @param index     index of the array to start laying down
+     * @param value     the integer to use least significant 16 bits
+     */
+    private static void lay16Bits(byte[] array, int index, int value){
+        array[index] = (byte) (value);
+        array[index + 1] = (byte) (value >> 8);
+    }
+
+    /**
+     * Lays an int into 4 items of a byte array.
+     *
+     * Note that ordering is little-endian.
+     *
+     * @param array     the array to be modified
+     * @param index     index of the array to start laying down
+     * @param value     the integer to use
+     */
+    private static void lay32Bits(byte[] array, int index, int value){
+        for (int i = 0; i < 4; i++){
+            array[index + i] = (byte) (value >> (i * 8));
+        }
+    }
+
+    /**
+     * Lays a long int into 8 items of a byte array.
+     *
+     * Note that ordering is little-endian.
+     *
+     * @param array     the array to be modified
+     * @param index     index of the array to start laying down
+     * @param value     the integer to use
+     */
+    private static void lay64bits(byte[] array, int index, long value){
+        for (int i = 0; i < 8; i++){
+            array[index + i] = (byte) (value >> (i * 8));
+        }
+    }
+}
diff --git a/tests/tests/media/src/android/media/cts/Vp8EncoderTest.java b/tests/tests/media/src/android/media/cts/Vp8EncoderTest.java
new file mode 100644
index 0000000..308fb98c
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/Vp8EncoderTest.java
@@ -0,0 +1,288 @@
+/*
+ * 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 android.media.cts;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaFormat;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.cts.media.R;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Basic verification test for vp8 encoder.
+ *
+ * A raw yv12 stream is encoded and written to an IVF
+ * file, which is later decoded by vp8 decoder to verify
+ * frames are at least decodable.
+ */
+public class Vp8EncoderTest extends AndroidTestCase {
+
+    private static final String TAG = "VP8EncoderTest";
+    private static final String VP8_MIME = "video/x-vnd.on2.vp8";
+    private static final String VPX_DECODER_NAME = "OMX.google.vpx.decoder";
+    private static final String VPX_ENCODER_NAME = "OMX.google.vpx.encoder";
+    private static final String BASIC_IVF = "video_176x144_vp8_basic.ivf";
+    private static final long DEFAULT_TIMEOUT_US = 5000;
+
+    private Resources mResources;
+    private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
+    private ByteBuffer[] mInputBuffers;
+    private ByteBuffer[] mOutputBuffers;
+
+    @Override
+    public void setContext(Context context) {
+        super.setContext(context);
+        mResources = mContext.getResources();
+    }
+
+    /**
+     * A basic test for VP8 encoder.
+     *
+     * Encodes a raw stream with default configuration options,
+     * and then decodes it to verify the bitstream.
+     */
+    public void testBasic() throws Exception {
+        encode(BASIC_IVF,
+               R.raw.video_176x144_yv12,
+               176,  // width
+               144,  // height
+               30);  // framerate
+        decode(BASIC_IVF);
+    }
+
+
+    /**
+     * A basic check if an encoded stream is decodable.
+     *
+     * The most basic confirmation we can get about a frame
+     * being properly encoded is trying to decode it.
+     * (Especially in realtime mode encode output is non-
+     * deterministic, therefore a more thorough check like
+     * md5 sum comparison wouldn't work.)
+     *
+     * Indeed, MediaCodec will raise an IllegalStateException
+     * whenever vp8 decoder fails to decode a frame, and
+     * this test uses that fact to verify the bitstream.
+     *
+     * @param filename  The name of the IVF file containing encoded bitsream.
+     */
+    private void decode(String filename) throws Exception {
+        IvfReader ivf = null;
+        try {
+            ivf = new IvfReader(filename);
+            int frameWidth = ivf.getWidth();
+            int frameHeight = ivf.getHeight();
+            int frameCount = ivf.getFrameCount();
+
+            assertTrue(frameWidth > 0);
+            assertTrue(frameHeight > 0);
+            assertTrue(frameCount > 0);
+
+            MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME,
+                                                               ivf.getWidth(),
+                                                               ivf.getHeight());
+
+            Log.d(TAG, "Creating decoder");
+            MediaCodec decoder = MediaCodec.createByCodecName(VPX_DECODER_NAME);
+            decoder.configure(format,
+                              null,  // surface
+                              null,  // crypto
+                              0);  // flags
+            decoder.start();
+
+            mInputBuffers = decoder.getInputBuffers();
+            mOutputBuffers = decoder.getOutputBuffers();
+
+            // decode loop
+            int frameIndex = 0;
+            boolean sawOutputEOS = false;
+            boolean sawInputEOS = false;
+
+            while (!sawOutputEOS) {
+                if (!sawInputEOS) {
+                    int inputBufIndex = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
+                    if (inputBufIndex >= 0) {
+                        byte[] frame = ivf.readFrame(frameIndex);
+
+                        if (frameIndex == frameCount - 1) {
+                            sawInputEOS = true;
+                        }
+
+                        mInputBuffers[inputBufIndex].clear();
+                        mInputBuffers[inputBufIndex].put(frame);
+                        mInputBuffers[inputBufIndex].rewind();
+
+                        Log.d(TAG, "Decoding frame at index " + frameIndex);
+                        try {
+                            decoder.queueInputBuffer(
+                                    inputBufIndex,
+                                    0,  // offset
+                                    frame.length,
+                                    frameIndex,
+                                    sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+                        } catch (IllegalStateException ise) {
+                            //That is all what is passed from MediaCodec in case of
+                            //decode failure.
+                            fail("Failed to decode frame at index " + frameIndex);
+                        }
+                        frameIndex++;
+                    }
+                }
+
+                int result = decoder.dequeueOutputBuffer(mBufferInfo, DEFAULT_TIMEOUT_US);
+                if (result >= 0) {
+                    int outputBufIndex = result;
+                    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                        sawOutputEOS = true;
+                    }
+                    decoder.releaseOutputBuffer(outputBufIndex, false);
+                } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+                    mOutputBuffers = decoder.getOutputBuffers();
+                }
+            }
+            decoder.stop();
+            decoder.release();
+        } finally {
+            if (ivf != null) {
+                ivf.close();
+            }
+        }
+    }
+
+    /**
+     * A basic vp8 encode loop.
+     *
+     * MediaCodec will raise an IllegalStateException
+     * whenever vp8 encoder fails to encode a frame.
+     *
+     * In addition to that written IVF file can be tested
+     * to be decodable in order to verify the bitstream produced.
+     *
+     * Color format of input file should be YUV420, and frameWidth,
+     * frameHeight should be supplied correctly as raw input file doesn't
+     * include any header data.
+     *
+     * @param outputFilename  The name of the IVF file to write encoded bitsream
+     * @param rawInputFd      File descriptor for the raw input file (YUV420)
+     * @param frameWidth      Frame width of input file
+     * @param frameHeight     Frame height of input file
+     * @param frameRate       Frame rate of input file in frames per second
+     */
+    private void encode(String outputFilename, int rawInputFd,
+                       int frameWidth, int frameHeight, int frameRate) throws Exception {
+        int frameSize = frameWidth * frameHeight * 3 / 2;
+
+
+        // Create a media format signifying desired output
+        MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME, frameWidth, frameHeight);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, 100000);
+        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
+                          CodecCapabilities.COLOR_FormatYUV420Planar);
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
+
+        Log.d(TAG, "Creating encoder");
+        MediaCodec encoder;
+        encoder = MediaCodec.createByCodecName(VPX_ENCODER_NAME);
+        encoder.configure(format,
+                          null,  // surface
+                          null,  // crypto
+                          MediaCodec.CONFIGURE_FLAG_ENCODE);
+        encoder.start();
+
+        mInputBuffers = encoder.getInputBuffers();
+        mOutputBuffers = encoder.getOutputBuffers();
+
+        InputStream rawStream = null;
+        IvfWriter ivf = null;
+
+        try {
+            rawStream = mResources.openRawResource(rawInputFd);
+            ivf = new IvfWriter(outputFilename, frameWidth, frameHeight);
+            // encode loop
+            long presentationTimeUs = 0;
+            int frameIndex = 0;
+            boolean sawInputEOS = false;
+            boolean sawOutputEOS = false;
+
+            while (!sawOutputEOS) {
+                if (!sawInputEOS) {
+                    int inputBufIndex = encoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
+                    if (inputBufIndex >= 0) {
+                        byte[] frame = new byte[frameSize];
+                        int bytesRead = rawStream.read(frame);
+
+                        if (bytesRead == -1) {
+                            sawInputEOS = true;
+                            bytesRead = 0;
+                        }
+
+                        mInputBuffers[inputBufIndex].clear();
+                        mInputBuffers[inputBufIndex].put(frame);
+                        mInputBuffers[inputBufIndex].rewind();
+
+                        presentationTimeUs = (frameIndex * 1000000) / frameRate;
+                        Log.d(TAG, "Encoding frame at index " + frameIndex);
+                        encoder.queueInputBuffer(
+                                inputBufIndex,
+                                0,  // offset
+                                bytesRead,  // size
+                                presentationTimeUs,
+                                sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
+
+                        frameIndex++;
+                    }
+                }
+
+                int result = encoder.dequeueOutputBuffer(mBufferInfo, DEFAULT_TIMEOUT_US);
+                if (result >= 0) {
+                    int outputBufIndex = result;
+                    byte[] buffer = new byte[mBufferInfo.size];
+                    mOutputBuffers[outputBufIndex].rewind();
+                    mOutputBuffers[outputBufIndex].get(buffer, 0, mBufferInfo.size);
+
+                    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                        sawOutputEOS = true;
+                    } else {
+                        ivf.writeFrame(buffer, mBufferInfo.presentationTimeUs);
+                    }
+                    encoder.releaseOutputBuffer(outputBufIndex,
+                                                false);  // render
+                } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+                    mOutputBuffers = encoder.getOutputBuffers();
+                }
+            }
+
+            encoder.stop();
+            encoder.release();
+        } finally {
+            if (ivf != null) {
+                ivf.close();
+            }
+
+            if (rawStream != null) {
+                rawStream.close();
+            }
+        }
+    }
+}