1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media.mediaediting.cts;
18 
19 import static android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10;
20 import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10;
21 import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10;
22 import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus;
23 import static android.media.mediaediting.cts.FileUtil.assertFileHasColorTransfer;
24 import static com.google.common.truth.Truth.assertThat;
25 import static org.junit.Assert.assertFalse;
26 import static org.junit.Assert.assertNotNull;
27 
28 import android.content.Context;
29 import android.media.MediaFormat;
30 import android.net.Uri;
31 import android.platform.test.annotations.AppModeFull;
32 
33 import androidx.annotation.NonNull;
34 import androidx.media3.common.C;
35 import androidx.media3.common.ColorInfo;
36 import androidx.media3.common.MediaItem;
37 import androidx.media3.common.MimeTypes;
38 import androidx.media3.transformer.ExportException;
39 import androidx.media3.transformer.TransformationRequest;
40 import androidx.media3.transformer.Composition;
41 import androidx.media3.transformer.Transformer;
42 import androidx.test.core.app.ApplicationProvider;
43 
44 import com.android.compatibility.common.util.ApiTest;
45 import com.android.compatibility.common.util.Preconditions;
46 
47 import org.junit.Assume;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 import org.junit.runners.Parameterized;
51 
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collection;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Objects;
58 import java.util.stream.IntStream;
59 
60 /**
61  * Instrumentation tests for transform HDR to SDR ToneMapping for given inputs.
62  */
63 @AppModeFull(reason = "Instant apps cannot access the SD card")
64 @RunWith(Parameterized.class)
65 public final class TransformHdrToSdrToneMapTest {
66   private static final String MEDIA_DIR = WorkDir.getMediaDirString();
67   private static final HashMap<String, int[]> PROFILE_HDR_MAP = new HashMap<>();
68   private static final int[] HEVC_HDR_PROFILES = new int[] {HEVCProfileMain10,
69       HEVCProfileMain10HDR10, HEVCProfileMain10HDR10Plus};
70   private static final int[] AVC_HDR_PROFILES = new int[] {AVCProfileHigh10};
71   static {
PROFILE_HDR_MAP.put(MediaFormat.MIMETYPE_VIDEO_AVC, AVC_HDR_PROFILES)72     PROFILE_HDR_MAP.put(MediaFormat.MIMETYPE_VIDEO_AVC, AVC_HDR_PROFILES);
PROFILE_HDR_MAP.put(MediaFormat.MIMETYPE_VIDEO_HEVC, HEVC_HDR_PROFILES)73     PROFILE_HDR_MAP.put(MediaFormat.MIMETYPE_VIDEO_HEVC, HEVC_HDR_PROFILES);
74   }
75 
76   private final String mediaType;
77   private final String testFile;
78   private final String testId;
79 
TransformHdrToSdrToneMapTest(String mediaType, String testFile, String testId)80   public TransformHdrToSdrToneMapTest(String mediaType, String testFile, String testId) {
81     this.mediaType = mediaType;
82     this.testFile = testFile;
83     this.testId = testId;
84   }
85 
86   @Parameterized.Parameters(name = "{index}_{2}")
input()87   public static Collection<Object[]> input() {
88     // mediaType, clip, width, height, frameRate
89     final List<Object[]> exhaustiveArgsList = new ArrayList<>(Arrays.asList(new Object[][] {
90         // H264
91         {MimeTypes.VIDEO_H264, MediaEditingUtil.MKV_ASSET_H264_340W_280H_10BIT, "340x280_24fps"},
92         {MimeTypes.VIDEO_H264, MediaEditingUtil.MKV_ASSET_H264_520W_390H_10BIT, "520x390_24fps"},
93         {MimeTypes.VIDEO_H264, MediaEditingUtil.MKV_ASSET_H264_640W_360H_10BIT, "640x360_24fps"},
94         {MimeTypes.VIDEO_H264, MediaEditingUtil.MKV_ASSET_H264_800W_640H_10BIT, "800x640_24fps"},
95         {MimeTypes.VIDEO_H264, MediaEditingUtil.MKV_ASSET_H264_1280W_720H_10BIT, "1280x720_24fps"},
96         // Hevc
97         {MimeTypes.VIDEO_H265, MediaEditingUtil.MKV_ASSET_HEVC_340W_280H_5S_10BIT, "340x280_24fps"},
98         {MimeTypes.VIDEO_H265, MediaEditingUtil.MKV_ASSET_HEVC_520W_390H_5S_10BIT, "520x390_24fps"},
99         {MimeTypes.VIDEO_H265, MediaEditingUtil.MKV_ASSET_HEVC_640W_360H_5S_10BIT, "640x360_24fps"},
100         {MimeTypes.VIDEO_H265, MediaEditingUtil.MKV_ASSET_HEVC_800W_640H_5S_10BIT, "800x640_24fps"},
101         {MimeTypes.VIDEO_H265, MediaEditingUtil.MKV_ASSET_HEVC_1280W_720H_5S_10BIT,
102             "1280x720_24fps"},
103     }));
104     return prepareParamList(exhaustiveArgsList);
105   }
106 
prepareParamList(List<Object[]> exhaustiveArgsList)107   public static List<Object[]> prepareParamList(List<Object[]> exhaustiveArgsList) {
108     List<Object[]> argsList = new ArrayList<>();
109 
110     for (Object[] arg : exhaustiveArgsList) {
111       String codec = arg[0].toString();
112       // Trim the mime baseType with slash.
113       int lastIndex = codec.lastIndexOf('/');
114       if (lastIndex != -1) {
115         codec = codec.substring(lastIndex + 1);
116       }
117       String testID = String.format("transformHdrToSdr_%s_%s_10bit", codec, arg[2]);
118       arg[2] = testID;
119       argsList.add(arg);
120     }
121     return argsList;
122   }
123 
createTransformer(Context context)124   private static Transformer createTransformer(Context context) {
125     return new Transformer.Builder(context)
126         .setTransformationRequest(
127             new TransformationRequest.Builder()
128                 .setHdrMode(Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC)
129                 .build())
130         .addListener(
131             new Transformer.Listener() {
132               @Override
133               public void onFallbackApplied(
134                   @NonNull MediaItem inputMediaItem,
135                   @NonNull TransformationRequest originalTransformationRequest,
136                   @NonNull TransformationRequest fallbackTransformationRequest) {
137                 // Tone mapping flag shouldn't change in fallback when tone mapping is requested.
138                 assertThat(originalTransformationRequest.hdrMode)
139                     .isEqualTo(fallbackTransformationRequest.hdrMode);
140               }
141             })
142         .build();
143   }
144 
145   @ApiTest(apis = {"android.media.MediaCodec#configure",
146       "android.media.MediaCodec#createByCodecName",
147       "android.media.MediaCodecList#findDecoderForFormat",
148       "android.media.MediaFormat#createVideoFormat",
149       "android.media.MediaFormat#KEY_COLOR_TRANSFER_REQUEST"})
150   @Test
151   public void transformHdrToSdrTest() throws Exception {
152     Preconditions.assertTestFileExists(MEDIA_DIR + testFile);
153     Context context = ApplicationProvider.getApplicationContext();
154 
155     Assume.assumeTrue("Skipping transformTest for " + testId,
156         !AndroidTestUtil.skipAndLogIfFormatsUnsupported(context, testId,
157         /* inputFormat= */ MediaEditingUtil.getFormatForTestFile(testFile),
158         /* outputFormat= */ MediaEditingUtil.getFormatForTestFile(testFile)
159             .buildUpon()
160             .setColorInfo(ColorInfo.SDR_BT709_LIMITED)
161             .build()));
162 
163     Transformer transformer = createTransformer(context);
164     ExportTestResult transformationTestResult;
165     try {
166       transformationTestResult = new TransformerAndroidTestRunner.Builder(context, transformer)
167           .build()
168           .run(testId, MediaItem.fromUri(Uri.parse(MEDIA_DIR + testFile)));
169     } catch (ExportException exception) {
170       if (exception.getCause() != null
171           && (Objects.equals(
172                   exception.getCause().getMessage(),
173                   "Tone-mapping HDR is not supported on this device.")
174               || Objects.equals(
175                   exception.getCause().getMessage(),
176                   "Tone-mapping requested but not supported by the decoder."))) {
177         // Expected on devices without a tone-mapping plugin for this codec.
178         return;
179       }
180       throw exception;
181     }
182     assertFileHasColorTransfer(context, transformationTestResult.filePath, C.COLOR_TRANSFER_SDR);
183     int profile = MediaEditingUtil.getMuxedOutputProfile(transformationTestResult.filePath);
184     int[] profileArray = PROFILE_HDR_MAP.get(mediaType);
185     assertNotNull("Expected value to be not null", profileArray);
186     assertFalse(testId + " must not contain HDR profile after tone mapping",
187         IntStream.of(profileArray).anyMatch(x -> x == profile));
188   }
189 }
190