media: add MediaCodec tests for block model

Bug: 136283874
Test: atest CtsMediaTestCases:MediaCodecBlockModelTest
Change-Id: Id40d591d268526c17edd613e85f5b7db598853bf
diff --git a/tests/tests/media/src/android/media/cts/MediaCodecBlockModelTest.java b/tests/tests/media/src/android/media/cts/MediaCodecBlockModelTest.java
new file mode 100644
index 0000000..0364f02
--- /dev/null
+++ b/tests/tests/media/src/android/media/cts/MediaCodecBlockModelTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2020 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.res.AssetFileDescriptor;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodec.CodecException;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.cts.R;
+import android.platform.test.annotations.RequiresDevice;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import androidx.test.filters.SmallTest;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * MediaCodec tests with CONFIGURE_FLAG_USE_BLOCK_MODEL.
+ */
+@SmallTest
+@RequiresDevice
+public class MediaCodecBlockModelTest extends AndroidTestCase {
+    private static final String TAG = "MediaCodecBlockModelTest";
+    private static final boolean VERBOSE = false;           // lots of logging
+
+                                                            // H.264 Advanced Video Coding
+    private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+
+    private static final int APP_BUFFER_SIZE = 1024 * 1024;  // 1 MB
+
+    // The test should fail if the decoder never produces output frames for the input.
+    // Time out decoding, as we have no way to query whether the decoder will produce output.
+    private static final int DECODING_TIMEOUT_MS = 10000;
+
+    /**
+     * Tests whether decoding a short group-of-pictures succeeds. The test queues a few video frames
+     * then signals end-of-stream. The test fails if the decoder doesn't output the queued frames.
+     */
+    public void testDecodeShortInput() throws InterruptedException {
+        // Input buffers from this input video are queued up to and including the video frame with
+        // timestamp LAST_BUFFER_TIMESTAMP_US.
+        final int INPUT_RESOURCE_ID =
+                R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+        final long LAST_BUFFER_TIMESTAMP_US = 166666;
+
+        // The test should fail if the decoder never produces output frames for the truncated input.
+        // Time out decoding, as we have no way to query whether the decoder will produce output.
+        final int DECODING_TIMEOUT_MS = 2000;
+
+        final AtomicBoolean completed = new AtomicBoolean();
+        Thread videoDecodingThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                completed.set(runDecodeShortInput(
+                        INPUT_RESOURCE_ID,
+                        LAST_BUFFER_TIMESTAMP_US,
+                        true /* obtainBlockForEachBuffer */));
+            }
+        });
+        videoDecodingThread.start();
+        videoDecodingThread.join(DECODING_TIMEOUT_MS);
+        if (!completed.get()) {
+            throw new RuntimeException("timed out decoding to end-of-stream");
+        }
+
+        videoDecodingThread = new Thread(new Runnable() {
+            @Override
+            public void run() {
+                completed.set(runDecodeShortInput(
+                        INPUT_RESOURCE_ID,
+                        LAST_BUFFER_TIMESTAMP_US,
+                        false /* obtainBlockForEachBuffer */));
+            }
+        });
+        videoDecodingThread.start();
+        videoDecodingThread.join(DECODING_TIMEOUT_MS);
+        if (!completed.get()) {
+            throw new RuntimeException("timed out decoding to end-of-stream");
+        }
+    }
+
+    private boolean runDecodeShortInput(
+            int inputResourceId, long lastBufferTimestampUs, boolean obtainBlockForEachBuffer) {
+        final int NO_BUFFER_INDEX = -1;
+
+        OutputSurface outputSurface = null;
+        MediaExtractor mediaExtractor = null;
+        MediaCodec mediaCodec = null;
+        try {
+            outputSurface = new OutputSurface(1, 1);
+            mediaExtractor = getMediaExtractorForMimeType(inputResourceId, "video/");
+            MediaFormat mediaFormat =
+                    mediaExtractor.getTrackFormat(mediaExtractor.getSampleTrackIndex());
+            String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
+            if (!supportsCodec(mimeType, false)) {
+                Log.i(TAG, "No decoder found for mimeType= " + MIME_TYPE);
+                return true;
+            }
+            // TODO: b/147748978
+            mediaCodec = MediaCodec.createByCodecName("c2.android.avc.decoder");
+            final LinkedBlockingQueue<Integer> inputQueue = new LinkedBlockingQueue<>();
+            final LinkedBlockingQueue<Integer> outputQueue = new LinkedBlockingQueue<>();
+            mediaCodec.setCallback(new MediaCodec.Callback() {
+                @Override
+                public void onInputBufferAvailable(MediaCodec codec, int index) {
+                    inputQueue.offer(index);
+                }
+
+                @Override
+                public void onOutputBufferAvailable(
+                        MediaCodec codec, int index, MediaCodec.BufferInfo info) {
+                    outputQueue.offer(index);
+                }
+
+                @Override
+                public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
+                }
+
+                @Override
+                public void onError(MediaCodec codec, CodecException e) {
+                }
+            });
+            String[] codecNames = new String[]{ mediaCodec.getName() };
+            MediaCodec.LinearBlock block = MediaCodec.LinearBlock.obtain(
+                    APP_BUFFER_SIZE, codecNames);
+            ByteBuffer buffer = block.map();
+            int offset = 0;
+            int outputBufferIndex = NO_BUFFER_INDEX;
+            mediaCodec.configure(
+                    mediaFormat, outputSurface.getSurface(), null,
+                    MediaCodec.CONFIGURE_FLAG_USE_BLOCK_MODEL);
+            mediaCodec.start();
+            boolean eos = false;
+            boolean signaledEos = false;
+            while (!eos && !Thread.interrupted()) {
+                // Try to feed more data into the codec.
+                if (mediaExtractor.getSampleTrackIndex() != -1 && !signaledEos) {
+                    int bufferIndex = NO_BUFFER_INDEX;
+                    try {
+                        bufferIndex = inputQueue.take();
+                    } catch (InterruptedException e) {
+                        return false;
+                    }
+                    long size = mediaExtractor.getSampleSize();
+                    if (obtainBlockForEachBuffer && block != null) {
+                        block.recycle();
+                        block = MediaCodec.LinearBlock.obtain(Math.toIntExact(size), codecNames);
+                        buffer = block.map();
+                        offset = 0;
+                    }
+                    if (buffer.capacity() < size) {
+                        block.recycle();
+                        block = MediaCodec.LinearBlock.obtain(
+                                Math.toIntExact(size * 2), codecNames);
+                        buffer = block.map();
+                        offset = 0;
+                    } else if (buffer.capacity() - offset < size) {
+                        long capacity = buffer.capacity();
+                        block.recycle();
+                        block = MediaCodec.LinearBlock.obtain(
+                                Math.toIntExact(capacity), codecNames);
+                        buffer = block.map();
+                        offset = 0;
+                    }
+                    long timestampUs = mediaExtractor.getSampleTime();
+                    int written = mediaExtractor.readSampleData(buffer, offset);
+                    mediaExtractor.advance();
+                    signaledEos = mediaExtractor.getSampleTrackIndex() == -1
+                            || timestampUs == lastBufferTimestampUs;
+                    mediaCodec.getQueueRequest(bufferIndex).setLinearBlock(
+                            block,
+                            offset,
+                            written,
+                            timestampUs,
+                            signaledEos ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0).queue();
+                    offset += written;
+                }
+
+                // If we don't have an output buffer, try to get one now.
+                if (outputBufferIndex == NO_BUFFER_INDEX) {
+                    Integer index = null;
+                    try {
+                        index = outputQueue.poll(inputQueue.isEmpty() ? 10 : 0, TimeUnit.MILLISECONDS);
+                    } catch (InterruptedException e) {
+                        return false;
+                    }
+                    outputBufferIndex = (index == null) ? NO_BUFFER_INDEX : index;
+                }
+
+                if (outputBufferIndex != NO_BUFFER_INDEX) {
+                    MediaCodec.OutputFrame frame = mediaCodec.getOutputFrame(outputBufferIndex);
+                    eos = (frame.getFlags() & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+
+                    boolean render = (frame.getGraphicBlock() != null);
+                    mediaCodec.releaseOutputBuffer(outputBufferIndex, render);
+                    if (render) {
+                        outputSurface.awaitNewImage();
+                    }
+
+                    outputBufferIndex = NO_BUFFER_INDEX;
+                }
+            }
+
+            block.recycle();
+            return eos;
+        } catch (IOException e) {
+            throw new RuntimeException("error reading input resource", e);
+        } finally {
+            if (mediaCodec != null) {
+                mediaCodec.stop();
+                mediaCodec.release();
+            }
+            if (mediaExtractor != null) {
+                mediaExtractor.release();
+            }
+            if (outputSurface != null) {
+                outputSurface.release();
+            }
+        }
+    }
+
+    private MediaExtractor getMediaExtractorForMimeType(int resourceId, String mimeTypePrefix)
+            throws IOException {
+        MediaExtractor mediaExtractor = new MediaExtractor();
+        AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(resourceId);
+        try {
+            mediaExtractor.setDataSource(
+                    afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+        } finally {
+            afd.close();
+        }
+        int trackIndex;
+        for (trackIndex = 0; trackIndex < mediaExtractor.getTrackCount(); trackIndex++) {
+            MediaFormat trackMediaFormat = mediaExtractor.getTrackFormat(trackIndex);
+            if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
+                mediaExtractor.selectTrack(trackIndex);
+                break;
+            }
+        }
+        if (trackIndex == mediaExtractor.getTrackCount()) {
+            throw new IllegalStateException("couldn't get a video track");
+        }
+
+        return mediaExtractor;
+    }
+
+    private static boolean supportsCodec(String mimeType, boolean encoder) {
+        MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
+        for (MediaCodecInfo info : list.getCodecInfos()) {
+            if (encoder != info.isEncoder()) {
+                continue;
+            }
+
+            for (String type : info.getSupportedTypes()) {
+                if (type.equalsIgnoreCase(mimeType)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}