mediav2 CTS: Add test to validate feature DynamicColorAspects

Bug: 325468696
Test: atest android.mediav2.cts.DecoderDynamicColorAspectTest

Change-Id: I1a153f027685516e80c5a3c270696e0c677727fc
diff --git a/tests/media/src/android/mediav2/cts/DecoderDynamicColorAspectTest.java b/tests/media/src/android/mediav2/cts/DecoderDynamicColorAspectTest.java
new file mode 100644
index 0000000..7207737
--- /dev/null
+++ b/tests/media/src/android/mediav2/cts/DecoderDynamicColorAspectTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2024 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.mediav2.cts;
+
+import static android.media.codec.Flags.FLAG_DYNAMIC_COLOR_ASPECTS;
+import static android.mediav2.common.cts.CodecTestBase.SupportClass.CODEC_OPTIONAL;
+
+import static org.junit.Assert.assertTrue;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.mediav2.common.cts.CodecDecoderTestBase;
+import android.mediav2.common.cts.CodecTestBase;
+import android.mediav2.common.cts.OutputManager;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.Range;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.compatibility.common.util.ApiTest;
+
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Test video decoders support for feature FEATURE_DynamicColorAspects.
+ * <p>
+ * The test decodes multiple clips that are configured with different color space attributes
+ * (color-primaries, color-standard and color-transfer), serially. The test seeks for output
+ * format at every dequeueOutput() call. The test expects the primaries, standard, transfer
+ * characteristics received shall match the clips attributes.
+ * <p>
+ * Certain media types cannot hold color aspects information in the bitstream (vp8/vp9/...). For
+ * these media types this information shall be configured via setParameters(Bundle). The test
+ * expects the component to relay this information correctly in the output format of each buffer
+ * to the client.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName = "VanillaIceCream")
+@RequiresFlagsEnabled(FLAG_DYNAMIC_COLOR_ASPECTS)
+@RunWith(Parameterized.class)
+@AppModeFull(reason = "Instant apps cannot access the SD card")
+public class DecoderDynamicColorAspectTest extends CodecDecoderTestBase {
+    private static final String LOG_TAG = DecoderDynamicColorAspectTest.class.getSimpleName();
+    private static final String MEDIA_DIR = WorkDir.getMediaDirString();
+
+    private static class MediaAndColorSpaceAttrib {
+        public final String mTestFile;
+        public final int mColorRange;
+        public final int mColorStandard;
+        public final int mColorTransfer;
+
+        MediaAndColorSpaceAttrib(String testFile, int colorRange, int colorStandard,
+                int colorTransfer) {
+            mTestFile = testFile;
+            mColorRange = colorRange;
+            mColorStandard = colorStandard;
+            mColorTransfer = colorTransfer;
+        }
+    }
+
+    private final ArrayList<MediaAndColorSpaceAttrib> mMediaAndColorSpaceAttribList;
+    private final HashMap<Range<Integer>, MediaAndColorSpaceAttrib> mRangeMediaColorSpaceMap =
+            new HashMap<>();
+    private long mMaxPts = 0;
+
+    public DecoderDynamicColorAspectTest(String decoder, String mediaType,
+            ArrayList<MediaAndColorSpaceAttrib> mediaAndColorSpaceAttrib, String allTestParams) {
+        super(decoder, mediaType, null, allTestParams);
+        mMediaAndColorSpaceAttribList = mediaAndColorSpaceAttrib;
+    }
+
+    @Parameterized.Parameters(name = "{index}_{0}_{1}")
+    public static Collection<Object[]> input() {
+        final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
+                {MediaFormat.MIMETYPE_VIDEO_AVC, new ArrayList<>(Arrays.asList(
+                        new MediaAndColorSpaceAttrib(
+                                "bbb_qcif_color_bt709_lr_sdr_avc.mp4",
+                                MediaFormat.COLOR_RANGE_LIMITED,
+                                MediaFormat.COLOR_STANDARD_BT709,
+                                MediaFormat.COLOR_TRANSFER_SDR_VIDEO),
+                        new MediaAndColorSpaceAttrib(
+                                "bbb_qcif_color_bt709_lr_srgb_avc.mp4",
+                                MediaFormat.COLOR_RANGE_LIMITED,
+                                MediaFormat.COLOR_STANDARD_BT709,
+                                /* MediaFormat.COLOR_TRANSFER_SRGB */ 2)))},
+                {MediaFormat.MIMETYPE_VIDEO_HEVC, new ArrayList<>(Arrays.asList(
+                        new MediaAndColorSpaceAttrib(
+                                "bbb_qcif_color_bt709_lr_sdr_hevc.mp4",
+                                MediaFormat.COLOR_RANGE_LIMITED,
+                                MediaFormat.COLOR_STANDARD_BT709,
+                                MediaFormat.COLOR_TRANSFER_SDR_VIDEO),
+                        new MediaAndColorSpaceAttrib(
+                                "bbb_qcif_color_bt601_625_fr_gamma22_hevc.mp4",
+                                MediaFormat.COLOR_RANGE_FULL,
+                                MediaFormat.COLOR_STANDARD_BT601_PAL,
+                                /* MediaFormat.COLOR_TRANSFER_GAMMA2_2 */ 4)))},
+                {MediaFormat.MIMETYPE_VIDEO_AV1, new ArrayList<>(Arrays.asList(
+                        new MediaAndColorSpaceAttrib(
+                                "bbb_qcif_color_bt709_lr_sdr_av1.mp4",
+                                MediaFormat.COLOR_RANGE_LIMITED,
+                                MediaFormat.COLOR_STANDARD_BT709,
+                                MediaFormat.COLOR_TRANSFER_SDR_VIDEO),
+                        new MediaAndColorSpaceAttrib(
+                                "bbb_qcif_color_bt601_625_fr_gamma22_av1.mp4",
+                                MediaFormat.COLOR_RANGE_FULL,
+                                MediaFormat.COLOR_STANDARD_BT601_PAL,
+                                /* MediaFormat.COLOR_TRANSFER_GAMMA2_2 */ 4)))},
+        });
+        return CodecTestBase.prepareParamList(exhaustiveArgsList, false, false, true, false);
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        Assume.assumeTrue("codec: " + mCodecName + " does not support"
+                + " FEATURE_DynamicColorAspects", isFeatureSupported(mCodecName, mMediaType,
+                MediaCodecInfo.CodecCapabilities.FEATURE_DynamicColorAspects));
+        ArrayList<MediaFormat> formats = new ArrayList<>();
+        for (MediaAndColorSpaceAttrib mediaAndColorSpaceAttrib : mMediaAndColorSpaceAttribList) {
+            formats.add(setUpSource(MEDIA_DIR + mediaAndColorSpaceAttrib.mTestFile));
+            mExtractor.release();
+        }
+        checkFormatSupport(mCodecName, mMediaType, false, formats, null, CODEC_OPTIONAL);
+    }
+
+    private MediaFormat createInputList(MediaFormat format, ByteBuffer buffer,
+            ArrayList<MediaCodec.BufferInfo> list, int offset, long ptsOffset,
+            ArrayList<Long> inpPtsList) {
+        if (hasCSD(format)) {
+            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+            bufferInfo.offset = offset;
+            bufferInfo.size = 0;
+            // For some devices with VNDK versions till Android U, sending a zero
+            // timestamp for CSD results in out of order timestamps at the output.
+            // For devices with VNDK versions > Android U, codecs are expected to
+            // handle CSD buffers with timestamp set to zero.
+            bufferInfo.presentationTimeUs = VNDK_IS_AT_MOST_U ? ptsOffset : 0;
+            bufferInfo.flags = MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
+            for (int i = 0; ; i++) {
+                String csdKey = "csd-" + i;
+                if (format.containsKey(csdKey)) {
+                    ByteBuffer csdBuffer = format.getByteBuffer(csdKey);
+                    bufferInfo.size += csdBuffer.limit();
+                    buffer.put(csdBuffer);
+                    format.removeKey(csdKey);
+                } else break;
+            }
+            list.add(bufferInfo);
+            offset += bufferInfo.size;
+        }
+        while (true) {
+            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+            bufferInfo.size = mExtractor.readSampleData(buffer, offset);
+            if (bufferInfo.size < 0) break;
+            bufferInfo.offset = offset;
+            bufferInfo.presentationTimeUs = ptsOffset + mExtractor.getSampleTime();
+            mMaxPts = Math.max(mMaxPts, bufferInfo.presentationTimeUs);
+            int flags = mExtractor.getSampleFlags();
+            bufferInfo.flags = 0;
+            if ((flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
+                bufferInfo.flags |= MediaCodec.BUFFER_FLAG_KEY_FRAME;
+            }
+            if (((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0)
+                    && ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_PARTIAL_FRAME) == 0)) {
+                if (!inpPtsList.contains(bufferInfo.presentationTimeUs)) {
+                    inpPtsList.add(bufferInfo.presentationTimeUs);
+                }
+            }
+            list.add(bufferInfo);
+            mExtractor.advance();
+            offset += bufferInfo.size;
+        }
+        buffer.clear();
+        buffer.position(offset);
+        return format;
+    }
+
+    @Override
+    protected void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+            mSawOutputEOS = true;
+        }
+        if (info.size > 0 && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
+            mOutputBuff.saveOutPTS(info.presentationTimeUs);
+            boolean validateAttrib = false;
+            for (Range<Integer> range : mRangeMediaColorSpaceMap.keySet()) {
+                if (range.contains(mOutputCount)) {
+                    MediaAndColorSpaceAttrib mediaAndColorSpaceAttrib =
+                            mRangeMediaColorSpaceMap.get(range);
+                    validateColorAspects(mCodec.getOutputFormat(bufferIndex),
+                            mediaAndColorSpaceAttrib.mColorRange,
+                            mediaAndColorSpaceAttrib.mColorStandard,
+                            mediaAndColorSpaceAttrib.mColorTransfer);
+                    validateAttrib = true;
+                    break;
+                }
+            }
+            assertTrue("unable to validate color space attributes for output frame id : "
+                    + mOutputCount + " \n" + mTestConfig + mTestEnv, validateAttrib);
+            mOutputCount++;
+        }
+        mCodec.releaseOutputBuffer(bufferIndex, mSurface != null);
+    }
+
+    /**
+     * Check description of class {@link DecoderDynamicColorAspectTest}
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM,
+            codeName = "VanillaIceCream")
+    @ApiTest(apis = {"android.media.MediaFormat#KEY_COLOR_RANGE",
+            "android.media.MediaFormat#KEY_COLOR_STANDARD",
+            "android.media.MediaFormat#KEY_COLOR_TRANSFER",
+            "android.media.MediaCodecInfo.CodecCapabilities#FEATURE_DynamicColorAspects"})
+    @LargeTest
+    @Test(timeout = PER_TEST_TIMEOUT_LARGE_TEST_MS)
+    public void testDynamicColorAspect() throws IOException, InterruptedException {
+        int totalSize = 0;
+        for (MediaAndColorSpaceAttrib mediaAndColorSpaceAttrib : mMediaAndColorSpaceAttribList) {
+            File file = new File(MEDIA_DIR + mediaAndColorSpaceAttrib.mTestFile);
+            totalSize += (int) file.length();
+        }
+        long ptsOffset = 0;
+        int buffOffset = 0;
+        ArrayList<MediaFormat> formats = new ArrayList<>();
+        ArrayList<MediaCodec.BufferInfo> list = new ArrayList<>();
+        ArrayList<Long> inpPtsList = new ArrayList<>();
+        ByteBuffer buffer = ByteBuffer.allocate(totalSize);
+        for (MediaAndColorSpaceAttrib mediaAndColorSpaceAttrib : mMediaAndColorSpaceAttribList) {
+            int lower = inpPtsList.size();
+            formats.add(createInputList(setUpSource(MEDIA_DIR + mediaAndColorSpaceAttrib.mTestFile),
+                    buffer, list, buffOffset, ptsOffset, inpPtsList));
+            mExtractor.release();
+            ptsOffset = mMaxPts + 1000000L;
+            buffOffset = (list.get(list.size() - 1).offset) + (list.get(list.size() - 1).size);
+            mRangeMediaColorSpaceMap.put(new Range<>(lower, inpPtsList.size() - 1),
+                    mediaAndColorSpaceAttrib);
+        }
+        mSaveToMem = false;
+        mOutputBuff = new OutputManager();
+        mCodec = MediaCodec.createByCodecName(mCodecName);
+        configureCodec(formats.get(0), false, true, false);
+        mCodec.start();
+        doWork(buffer, list);
+        queueEOS();
+        waitForAllOutputs();
+        mCodec.stop();
+        mCodec.release();
+    }
+}