enc stat CTS for average QP, only check QP vs Bitrate

This test invokes video encoder only and check whether
lower average QP gives higher bitrate video.

Bug: 205853668
Test: CTS
Change-Id: I6566dc930019a0261eb6ffe477c4a0b22f6e94c8
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTest.java b/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTest.java
index c7570e8..1dd7651 100644
--- a/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTest.java
+++ b/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTest.java
@@ -175,7 +175,8 @@
                     targetBitrate,
                     true);
             ArrayList<ByteBuffer> codecConfigs = new ArrayList<>();
-            ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params, codecConfigs);
+            VideoEncodeOutput videoEncodeOutput = encode(params, codecConfigs);
+            ArrayList<MediaCodec.BufferInfo> bufInfo = videoEncodeOutput.bufferInfo;
             if (bufInfo == null) {
                 continue;
             }
@@ -236,7 +237,8 @@
                 BITRATE,
                 syncEncoding);
         ArrayList<ByteBuffer> codecConfigs = new ArrayList<>();
-        ArrayList<MediaCodec.BufferInfo> bufInfos = encodeAsync(params, codecConfigs);
+        VideoEncodeOutput videoEncodeOutput = encodeAsync(params, codecConfigs);
+        ArrayList<MediaCodec.BufferInfo> bufInfos = videoEncodeOutput.bufferInfo;
         if (bufInfos == null) {
             Log.i(TAG, "SKIPPING testAsyncEncoding(): no suitable encoder found");
             return;
@@ -263,7 +265,8 @@
                 BITRATE,
                 syncEncoding);
         codecConfigs.clear();
-        bufInfos = encode(params, codecConfigs);
+        videoEncodeOutput = encode(params, codecConfigs);
+        bufInfos = videoEncodeOutput.bufferInfo;
         if (bufInfos == null) {
             Log.i(TAG, "SKIPPING testAsyncEncoding(): no suitable encoder found");
             return;
@@ -312,7 +315,8 @@
         params.syncFrameInterval = encodeSeconds * FPS;
         params.syncForceFrameInterval = FPS;
         params.useNdk = useNdk;
-        ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params);
+        VideoEncodeOutput videoEncodeOutput = encode(params);
+        ArrayList<MediaCodec.BufferInfo> bufInfo = videoEncodeOutput.bufferInfo;
         if (bufInfo == null) {
             Log.i(TAG, "SKIPPING testSyncFrame(): no suitable encoder found");
             return;
@@ -376,7 +380,8 @@
         }
 
         params.useNdk = useNdk;
-        ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params);
+        VideoEncodeOutput videoEncodeOutput = encode(params);
+        ArrayList<MediaCodec.BufferInfo> bufInfo = videoEncodeOutput.bufferInfo;
         if (bufInfo == null) {
             Log.i(TAG, "SKIPPING testDynamicBitrateChange(): no suitable encoder found");
             return;
@@ -453,9 +458,11 @@
                  try {
                      ArrayList<MediaCodec.BufferInfo> bufInfo;
                      if (codecConfigs.isEmpty()) {
-                         bufInfo = encode(params, codecConfigs);
+                         VideoEncodeOutput videoEncodeOutput = encode(params, codecConfigs);
+                         bufInfo = videoEncodeOutput.bufferInfo;
                      } else {
-                         bufInfo = encode(params);
+                         VideoEncodeOutput videoEncodeOutput = encode(params);
+                         bufInfo = videoEncodeOutput.bufferInfo;
                      }
                      VideoEncodingStatistics statistics = computeEncodingStatistics(bufInfo);
                      bitrate[0] = statistics.mAverageBitrate;
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTestBase.java b/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTestBase.java
index 3aefbab..cbe3966 100644
--- a/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTestBase.java
+++ b/tests/tests/media/codec/src/android/media/codec/cts/VideoCodecTestBase.java
@@ -247,6 +247,33 @@
         boolean runInLooperThread;
         // Flag if use NdkMediaCodec
         boolean useNdk;
+        // Encoding Statistics Level
+        // 0: None, 1: Average block QP and picture type of a frame
+        public int encodingStatisticsLevel;
+    }
+
+    /**
+     * Encoding Statistics for a whole sequence
+     */
+    protected class EncodingStatisticsInfo {
+        public float averageSeqQp = 0; // Average qp of a whole sequence,
+                                         // i.e. average of 'per-frame average block QP'
+        public int encodedFrames = 0; // # of encoded frames,
+                                       // i.e. # of average_block_qp is reported
+    }
+
+    /**
+     * Encoding Statistics for a whole sequence
+     */
+    protected class VideoEncodeOutput{
+        public ArrayList<MediaCodec.BufferInfo> bufferInfo;
+        public EncodingStatisticsInfo encStat;
+
+        VideoEncodeOutput(ArrayList<MediaCodec.BufferInfo> bufferInfo,
+            EncodingStatisticsInfo encStat) {
+            bufferInfo = bufferInfo;
+            encStat = encStat;
+        }
     }
 
     private String getCodecSuffix(String codecMimeType) {
@@ -795,19 +822,22 @@
 
         private InputStream mYuvStream;
         private int mInputFrameIndex;
+        private final EncodingStatisticsInfo mEncStatInfo;
 
         MediaEncoderAsyncHelper(
                 EncoderOutputStreamParameters streamParams,
                 CodecProperties properties,
                 ArrayList<MediaCodec.BufferInfo> bufferInfos,
                 IvfWriter ivf,
-                ArrayList<ByteBuffer> codecConfigs)
+                ArrayList<ByteBuffer> codecConfigs,
+                EncodingStatisticsInfo encStatInfo)
                 throws Exception {
             mStreamParams = streamParams;
             mProperties = properties;
             mBufferInfos = bufferInfos;
             mIvf = ivf;
             mCodecConfigs = codecConfigs;
+            mEncStatInfo = encStatInfo;
 
             int srcFrameSize = streamParams.frameWidth * streamParams.frameHeight * 3 / 2;
             mSrcFrame = new byte[srcFrameSize];
@@ -884,6 +914,11 @@
             }
             return false;
         }
+
+        public void saveAvgQp(int avg_qp) {
+            mEncStatInfo.averageSeqQp += (float) avg_qp;
+            ++mEncStatInfo.encodedFrames;  // Note: Duplicated info to  mOutputFrameIndex
+        }
     }
 
     /**
@@ -1012,6 +1047,13 @@
                 out.flags = info.flags;
                 out.outputGenerated = true;
 
+                MediaFormat format = codec.getOutputFormat(index);
+                if (format.containsKey(MediaFormat.KEY_VIDEO_QP_AVERAGE)) {
+                    int avgQp = format.getInteger(MediaFormat.KEY_VIDEO_QP_AVERAGE);
+                    // Copy per-frame avgQp to sequence level buffer
+                    mHelper.saveAvgQp(avgQp);
+                }
+
                 if (mHelper.saveOutputFrame(out)) {
                     // output EOS
                     signalCompletion();
@@ -1347,7 +1389,7 @@
     /**
      * @see #encode(EncoderOutputStreamParameters, ArrayList<ByteBuffer>)
      */
-    protected ArrayList<MediaCodec.BufferInfo> encode(
+    protected VideoEncodeOutput encode(
             EncoderOutputStreamParameters streamParams) throws Exception {
         return encode(streamParams, new ArrayList<ByteBuffer>());
     }
@@ -1367,13 +1409,16 @@
      *
      * @param streamParams  Structure with encoder parameters
      * @param codecConfigs  List to be filled with codec config buffers
-     * @return              Returns array of encoded frames information for each frame.
+     * @return              Returns VideoEncodeOutput, which consists of
+     *                      array of encoded frames information for each frame and Encoding
+     *                      Statistics Information.
      */
-    protected ArrayList<MediaCodec.BufferInfo> encode(
+    protected VideoEncodeOutput encode(
             EncoderOutputStreamParameters streamParams,
             ArrayList<ByteBuffer> codecConfigs) throws Exception {
 
         ArrayList<MediaCodec.BufferInfo> bufferInfos = new ArrayList<MediaCodec.BufferInfo>();
+        EncodingStatisticsInfo encStatInfo = new EncodingStatisticsInfo();
         Log.d(TAG, "Source resolution: "+streamParams.frameWidth + " x " +
                 streamParams.frameHeight);
         int bitrate = streamParams.bitrateSet[0];
@@ -1404,6 +1449,11 @@
         int syncFrameInterval = (streamParams.syncFrameInterval + streamParams.frameRate/2) /
                 streamParams.frameRate;
         format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, syncFrameInterval);
+        if (streamParams.encodingStatisticsLevel !=
+                MediaFormat.VIDEO_ENCODING_STATISTICS_LEVEL_NONE) {
+            format.setInteger(MediaFormat.KEY_VIDEO_ENCODING_STATISTICS_LEVEL,
+                    streamParams.encodingStatisticsLevel);
+        }
 
         // Create encoder
         Log.d(TAG, "Creating encoder " + properties.codecName +
@@ -1529,7 +1579,7 @@
         ivf.close();
         yuvStream.close();
 
-        return bufferInfos;
+        return new VideoEncodeOutput(bufferInfos, encStatInfo);
     }
 
     /**
@@ -1546,9 +1596,11 @@
      *
      * @param streamParams  Structure with encoder parameters
      * @param codecConfigs  List to be filled with codec config buffers
-     * @return              Returns array of encoded frames information for each frame.
+     * @return              Returns VideoEncodeOutput, which consists of
+     *                      array of encoded frames information for each frame and Encoding
+     *                      Statistics Information.
      */
-    protected ArrayList<MediaCodec.BufferInfo> encodeAsync(
+    protected VideoEncodeOutput encodeAsync(
             EncoderOutputStreamParameters streamParams,
             ArrayList<ByteBuffer> codecConfigs) throws Exception {
         if (!streamParams.runInLooperThread) {
@@ -1556,6 +1608,7 @@
         }
 
         ArrayList<MediaCodec.BufferInfo> bufferInfos = new ArrayList<MediaCodec.BufferInfo>();
+        EncodingStatisticsInfo encStatInfo = new EncodingStatisticsInfo();
         Log.d(TAG, "Source resolution: "+streamParams.frameWidth + " x " +
                 streamParams.frameHeight);
         int bitrate = streamParams.bitrateSet[0];
@@ -1584,7 +1637,11 @@
         int syncFrameInterval = (streamParams.syncFrameInterval + streamParams.frameRate/2) /
                 streamParams.frameRate;
         format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, syncFrameInterval);
-
+        if (streamParams.encodingStatisticsLevel !=
+                MediaFormat.VIDEO_ENCODING_STATISTICS_LEVEL_NONE) {
+            format.setInteger(MediaFormat.KEY_VIDEO_ENCODING_STATISTICS_LEVEL,
+                    MediaFormat.VIDEO_ENCODING_STATISTICS_LEVEL_1);
+        }
         // Create encoder
         Log.d(TAG, "Creating encoder " + properties.codecName +
                 ". Color format: 0x" + Integer.toHexString(properties.colorFormat)+ " : " +
@@ -1598,7 +1655,7 @@
 
         MediaEncoderAsync codec = new MediaEncoderAsync();
         MediaEncoderAsyncHelper helper = new MediaEncoderAsyncHelper(
-                streamParams, properties, bufferInfos, ivf, codecConfigs);
+                streamParams, properties, bufferInfos, ivf, codecConfigs, encStatInfo);
 
         codec.setAsyncHelper(helper);
         codec.createCodec(0, properties.codecName, format,
@@ -1608,7 +1665,7 @@
         codec.deleteCodec();
         ivf.close();
 
-        return bufferInfos;
+        return new VideoEncodeOutput(bufferInfos, encStatInfo);
     }
 
     /**
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/VideoEncodingStatisticsTest.java b/tests/tests/media/codec/src/android/media/codec/cts/VideoEncodingStatisticsTest.java
new file mode 100644
index 0000000..10a18dd
--- /dev/null
+++ b/tests/tests/media/codec/src/android/media/codec/cts/VideoEncodingStatisticsTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2022 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.codec.cts;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
+import android.media.cts.MediaCodecWrapper;
+import android.media.cts.MediaHeavyPresubmitTest;
+import android.platform.test.annotations.AppModeFull;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.MediaUtils;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Verification test for video encoding statistics.
+ *
+ * Check whether a higher bitrate gives a lower average QP reported from encoder
+ *
+ */
+@MediaHeavyPresubmitTest
+@AppModeFull(reason = "TODO: evaluate and port to instant")
+@RunWith(Parameterized.class)
+public class VideoEncodingStatisticsTest extends VideoCodecTestBase {
+
+    private static final String ENCODED_IVF_BASE = "football";
+    private static final String INPUT_YUV = null;
+    private static final String OUTPUT_YUV = SDCARD_DIR + File.separator +
+            ENCODED_IVF_BASE + "_out.yuv";
+
+    // YUV stream properties.
+    private static final int WIDTH = 320;
+    private static final int HEIGHT = 240;
+    private static final int FPS = 30;
+    // Default encoding bitrate.
+    private static final int BITRATE = 400000;
+    // List of bitrates used in quality and basic bitrate tests.
+    private static final int[] TEST_BITRATES_SET = { 300000, 500000, 700000, 900000 };
+
+    private static final String CODEC_PREFIX_KEY = "codec-prefix";
+    private static final String mCodecPrefix;
+
+    @Parameterized.Parameter(0)
+    public String mCodecName;
+
+    @Parameterized.Parameter(1)
+    public String mCodecMimeType;
+
+    @Parameterized.Parameter(2)
+    public int mBitRateMode;
+
+    static {
+        android.os.Bundle args = InstrumentationRegistry.getArguments();
+        mCodecPrefix = args.getString(CODEC_PREFIX_KEY);
+    }
+
+    static private List<Object[]> prepareParamList(List<Object[]> exhaustiveArgsList) {
+        final List<Object[]> argsList = new ArrayList<>();
+        int argLength = exhaustiveArgsList.get(0).length;
+        for (Object[] arg : exhaustiveArgsList) {
+            String[] encodersForMime = MediaUtils.getEncoderNamesForMime((String) arg[0]);
+            for (String encoder : encodersForMime) {
+                if (mCodecPrefix != null && !encoder.startsWith(mCodecPrefix)) {
+                    continue;
+                }
+                Object[] testArgs = new Object[argLength + 1];
+                testArgs[0] = encoder;
+                System.arraycopy(arg, 0, testArgs, 1, argLength);
+                argsList.add(testArgs);
+            }
+        }
+        return argsList;
+    }
+
+    @Parameterized.Parameters(name = "{index}({0}:{1}:{2})")
+    public static Collection<Object[]> input() {
+        final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
+                {AVC_MIME, VIDEO_ControlRateConstant},
+                {AVC_MIME, VIDEO_ControlRateVariable},
+                {HEVC_MIME, VIDEO_ControlRateConstant},
+                {HEVC_MIME, VIDEO_ControlRateVariable},
+        });
+        return prepareParamList(exhaustiveArgsList);
+    }
+
+    private static CodecCapabilities getCodecCapabilities(
+            String encoderName, String mime, boolean isEncoder) {
+        MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+        for (MediaCodecInfo codecInfo : mcl.getCodecInfos()) {
+            if (isEncoder != codecInfo.isEncoder()) {
+                continue;
+            }
+            if (encoderName.equals(codecInfo.getName())) {
+                return codecInfo.getCapabilitiesForType(mime);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Check whethera a higher bitrate gives a lower average QP
+     *
+     * Video streams with higher bitrate should have lower average qp.
+     */
+    private void testEncStatRateAvgQp(String codecName, String codecMimeType, int bitRateMode)
+            throws Exception {
+        int encodeSeconds = 9;      // Encoding sequence duration in seconds for each bitrate.
+        float[] avgSeqQp = new float[TEST_BITRATES_SET.length];
+        boolean[] completed = new boolean[TEST_BITRATES_SET.length];
+        boolean skipped = true;
+        ArrayList<MediaCodec.BufferInfo> bufInfos;
+
+        CodecCapabilities caps = getCodecCapabilities(codecName, codecMimeType, true);
+        Assume.assumeTrue(codecName + " does not support FEATURE_EncodingStatistics",
+           caps.isFeatureSupported(CodecCapabilities.FEATURE_EncodingStatistics));
+
+        for (int i = 0; i < TEST_BITRATES_SET.length; i++) {
+            EncoderOutputStreamParameters params = getDefaultEncodingParameters(
+                    INPUT_YUV,
+                    ENCODED_IVF_BASE,
+                    codecName,
+                    codecMimeType,
+                    encodeSeconds,
+                    WIDTH,
+                    HEIGHT,
+                    FPS,
+                    bitRateMode,
+                    TEST_BITRATES_SET[i],
+                    true);
+            // Enable encoding statistics at VIDEO_ENCODING_STATISTICS_LEVEL_1
+            params.encodingStatisticsLevel = MediaFormat.VIDEO_ENCODING_STATISTICS_LEVEL_1;
+            ArrayList<ByteBuffer> codecConfigs = new ArrayList<>();
+            VideoEncodeOutput videoEncodeOutput = encode(params, codecConfigs);
+            bufInfos = videoEncodeOutput.bufferInfo;
+            if (bufInfos == null) {
+                // parameters not supported, try other bitrates
+                completed[i] = false;
+                continue;
+            }
+            completed[i] = true;
+            skipped = false;
+            if (videoEncodeOutput.encStat.encodedFrames > 0) {
+                avgSeqQp[i] = (float) videoEncodeOutput.encStat.averageSeqQp
+                                      / videoEncodeOutput.encStat.encodedFrames;
+            }
+        }
+
+        if (skipped) {
+            Log.i(TAG, "SKIPPING testEncodingStatisticsAvgQp(): no bitrates supported");
+            return;
+        }
+
+        // First do a validity check - higher bitrates should results in lower QP.
+        for (int i = 1; i < TEST_BITRATES_SET.length; i++) {
+            if (!completed[i]) {
+                continue;
+            }
+            for (int j = 0; j < i; j++) {
+                if (!completed[j]) {
+                    continue;
+                }
+                double differenceBitrate = TEST_BITRATES_SET[i] - TEST_BITRATES_SET[j];
+                double differenceAvgQp = avgSeqQp[i] - avgSeqQp[j];
+                if (differenceBitrate * differenceAvgQp > 0) {
+                    throw new RuntimeException("Target bitrates: " +
+                            TEST_BITRATES_SET[j] + ", " + TEST_BITRATES_SET[i] +
+                            ". Average QP: "
+                            + avgSeqQp[j] + ", " + avgSeqQp[i]);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testEncodingStatisticsAvgQp() throws Exception {
+       testEncStatRateAvgQp(mCodecName, mCodecMimeType, mBitRateMode);
+   }
+}
+