1 /*
2  * Copyright (C) 2016 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 package android.media.decoder.cts;
17 
18 import static junit.framework.TestCase.assertTrue;
19 
20 import static org.junit.Assert.fail;
21 
22 import android.annotation.TargetApi;
23 import android.graphics.Bitmap;
24 import android.media.MediaFormat;
25 import android.media.cts.MediaHeavyPresubmitTest;
26 import android.media.cts.TestArgs;
27 import android.media.cts.TestUtils;
28 import android.os.Build;
29 import android.os.Environment;
30 import android.platform.test.annotations.AppModeFull;
31 import android.util.Log;
32 import android.view.View;
33 
34 import com.android.compatibility.common.util.ApiLevelUtil;
35 import com.android.compatibility.common.util.MediaUtils;
36 
37 import org.junit.After;
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 import org.junit.runners.Parameterized;
41 import org.junit.runners.Parameterized.Parameters;
42 
43 import java.io.File;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.lang.reflect.Field;
47 import java.util.ArrayList;
48 import java.util.Collection;
49 import java.util.List;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52 
53 @TargetApi(24)
54 @RunWith(Parameterized.class)
55 @MediaHeavyPresubmitTest
56 @AppModeFull(reason = "There should be no instant apps specific behavior related to accuracy")
57 public class DecodeAccuracyTest extends DecodeAccuracyTestBase {
58 
59     private static final boolean IS_AT_LEAST_U = ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU)
60             || ApiLevelUtil.codenameEquals("UpsideDownCake");
61     private static final boolean IS_BEFORE_U = !IS_AT_LEAST_U;
62 
63     private static final String TAG = DecodeAccuracyTest.class.getSimpleName();
64     private static final Field[] fields = R.raw.class.getFields();
65     private static final int ALLOWED_GREATEST_PIXEL_DIFFERENCE = 105;
66     private static final int OFFSET = 10;
67     private static final long PER_TEST_TIMEOUT_MS = 60000;
68     private static final String[] VIDEO_FILES = {
69         // 144p
70         "video_decode_accuracy_and_capability-h264_256x108_30fps.mp4",
71         "video_decode_accuracy_and_capability-h264_256x144_30fps.mp4",
72         "video_decode_accuracy_and_capability-h264_192x144_30fps.mp4",
73         "video_decode_accuracy_and_capability-h264_82x144_30fps.mp4",
74         "video_decode_accuracy_and_capability-vp9_256x108_30fps.webm",
75         "video_decode_accuracy_and_capability-vp9_256x144_30fps.webm",
76         "video_decode_accuracy_and_capability-vp9_192x144_30fps.webm",
77         "video_decode_accuracy_and_capability-vp9_82x144_30fps.webm",
78         // 240p
79         "video_decode_accuracy_and_capability-h264_426x182_30fps.mp4",
80         "video_decode_accuracy_and_capability-h264_426x240_30fps.mp4",
81         "video_decode_accuracy_and_capability-h264_320x240_30fps.mp4",
82         "video_decode_accuracy_and_capability-h264_136x240_30fps.mp4",
83         "video_decode_accuracy_and_capability-vp9_426x182_30fps.webm",
84         "video_decode_accuracy_and_capability-vp9_426x240_30fps.webm",
85         "video_decode_accuracy_and_capability-vp9_320x240_30fps.webm",
86         "video_decode_accuracy_and_capability-vp9_136x240_30fps.webm",
87         // 360p
88         "video_decode_accuracy_and_capability-h264_640x272_30fps.mp4",
89         "video_decode_accuracy_and_capability-h264_640x360_30fps.mp4",
90         "video_decode_accuracy_and_capability-h264_480x360_30fps.mp4",
91         "video_decode_accuracy_and_capability-h264_202x360_30fps.mp4",
92         "video_decode_accuracy_and_capability-vp9_640x272_30fps.webm",
93         "video_decode_accuracy_and_capability-vp9_640x360_30fps.webm",
94         "video_decode_accuracy_and_capability-vp9_480x360_30fps.webm",
95         "video_decode_accuracy_and_capability-vp9_202x360_30fps.webm",
96         // 480p
97         "video_decode_accuracy_and_capability-h264_854x362_30fps.mp4",
98         "video_decode_accuracy_and_capability-h264_854x480_30fps.mp4",
99         "video_decode_accuracy_and_capability-h264_640x480_30fps.mp4",
100         "video_decode_accuracy_and_capability-h264_270x480_30fps.mp4",
101         "video_decode_accuracy_and_capability-vp9_854x362_30fps.webm",
102         "video_decode_accuracy_and_capability-vp9_854x480_30fps.webm",
103         "video_decode_accuracy_and_capability-vp9_640x480_30fps.webm",
104         "video_decode_accuracy_and_capability-vp9_270x480_30fps.webm",
105         // 720p
106         "video_decode_accuracy_and_capability-h264_1280x544_30fps.mp4",
107         "video_decode_accuracy_and_capability-h264_1280x720_30fps.mp4",
108         "video_decode_accuracy_and_capability-h264_960x720_30fps.mp4",
109         "video_decode_accuracy_and_capability-h264_406x720_30fps.mp4",
110         "video_decode_accuracy_and_capability-vp9_1280x544_30fps.webm",
111         "video_decode_accuracy_and_capability-vp9_1280x720_30fps.webm",
112         "video_decode_accuracy_and_capability-vp9_960x720_30fps.webm",
113         "video_decode_accuracy_and_capability-vp9_406x720_30fps.webm",
114         // 1080p
115         "video_decode_accuracy_and_capability-h264_1920x818_30fps.mp4",
116         "video_decode_accuracy_and_capability-h264_1920x1080_30fps.mp4",
117         "video_decode_accuracy_and_capability-h264_1440x1080_30fps.mp4",
118         "video_decode_accuracy_and_capability-h264_608x1080_30fps.mp4",
119         "video_decode_accuracy_and_capability-vp9_1920x818_30fps.webm",
120         "video_decode_accuracy_and_capability-vp9_1920x1080_30fps.webm",
121         "video_decode_accuracy_and_capability-vp9_1440x1080_30fps.webm",
122         "video_decode_accuracy_and_capability-vp9_608x1080_30fps.webm",
123         // 1440p
124         "video_decode_accuracy_and_capability-h264_2560x1090_30fps.mp4",
125         "video_decode_accuracy_and_capability-h264_2560x1440_30fps.mp4",
126         "video_decode_accuracy_and_capability-h264_1920x1440_30fps.mp4",
127         "video_decode_accuracy_and_capability-h264_810x1440_30fps.mp4",
128         "video_decode_accuracy_and_capability-vp9_2560x1090_30fps.webm",
129         "video_decode_accuracy_and_capability-vp9_2560x1440_30fps.webm",
130         "video_decode_accuracy_and_capability-vp9_1920x1440_30fps.webm",
131         "video_decode_accuracy_and_capability-vp9_810x1440_30fps.webm",
132         // 2160p
133         "video_decode_accuracy_and_capability-h264_3840x1634_30fps.mp4",
134         "video_decode_accuracy_and_capability-h264_3840x2160_30fps.mp4",
135         "video_decode_accuracy_and_capability-h264_2880x2160_30fps.mp4",
136         "video_decode_accuracy_and_capability-h264_1216x2160_30fps.mp4",
137         "video_decode_accuracy_and_capability-vp9_3840x1634_30fps.webm",
138         "video_decode_accuracy_and_capability-vp9_3840x2160_30fps.webm",
139         "video_decode_accuracy_and_capability-vp9_2880x2160_30fps.webm",
140         "video_decode_accuracy_and_capability-vp9_1216x2160_30fps.webm",
141         // cropped
142         "video_decode_with_cropping-h264_520x360_30fps.mp4",
143         "video_decode_with_cropping-vp9_520x360_30fps.webm"
144     };
145 
146     private static final String INP_PREFIX = WorkDir.getMediaDirString() +
147             "assets/decode_accuracy/";
148 
149     private View videoView;
150     private VideoViewFactory videoViewFactory;
151     private String testName;
152     private String fileName;
153     private String decoderName;
154     private String methodName;
155     private SimplePlayer player;
156 
DecodeAccuracyTest(String decoderName, String fileName, String testName)157     public DecodeAccuracyTest(String decoderName, String fileName, String testName) {
158         this.testName = testName;
159         this.fileName = fileName;
160         this.decoderName = decoderName;
161     }
162 
163     @After
164     @Override
tearDown()165     public void tearDown() throws Exception {
166         if (player != null) {
167             player.release();
168         }
169         if (videoView != null) {
170             getHelper().cleanUpView(videoView);
171         }
172         if (videoViewFactory != null) {
173             videoViewFactory.release();
174         }
175         super.tearDown();
176     }
177 
178     @Parameters(name = "{index}({0}_{2})")
input()179     public static Collection<Object[]> input() throws IOException {
180         final List<Object[]> testParams = new ArrayList<>();
181         for (String file : VIDEO_FILES) {
182             Pattern regex = Pattern.compile("^\\w+-(\\w+)_\\d+fps\\.\\w+");
183             Matcher matcher = regex.matcher(file);
184             String testName = "";
185             if (matcher.matches()) {
186                 testName = matcher.group(1);
187             }
188             MediaFormat mediaFormat =
189                     MediaUtils.getTrackFormatForResource(INP_PREFIX + file, "video");
190             String mediaType = mediaFormat.getString(MediaFormat.KEY_MIME);
191             if (TestArgs.shouldSkipMediaType(mediaType)) {
192                 continue;
193             }
194             String[] componentNames = MediaUtils.getDecoderNamesForMime(mediaType);
195             for (String componentName : componentNames) {
196                 if (!MediaUtils.supports(componentName, mediaFormat)) {
197                     continue;
198                 }
199 
200                 // we only test the first codec that supports the format.
201                 // if that codec is not tested in this mode, we do NOT proceed to
202                 // later codecs in the list.
203                 //
204                 // this means:
205                 // in CTS, we'll test the HW codec.
206                 // in MCTS/MTS, we'll see the HW codec, skip testing it, but NOT fall through
207                 // to test any of the module-homed codecs.
208 
209                 if (!TestArgs.shouldSkipCodec(componentName)) {
210                     testParams.add(new Object[] {componentName, file, testName});
211                 }
212 
213                 // ignore any further codes that might handle this mediatype, even if we
214                 // chose not to test this first one.
215                 // NB: remove this break to change from "test first codec" to "test all codecs"
216                 break;
217             }
218         }
219         return testParams;
220     }
221 
222     @Test(timeout = PER_TEST_TIMEOUT_MS)
testGLViewDecodeAccuracy()223     public void testGLViewDecodeAccuracy() throws Exception {
224         this.methodName = "testGLViewDecodeAccuracy";
225         runTest(new GLSurfaceViewFactory(), new VideoFormat(fileName), decoderName);
226     }
227 
228     @Test(timeout = PER_TEST_TIMEOUT_MS)
testGLViewLargerHeightDecodeAccuracy()229     public void testGLViewLargerHeightDecodeAccuracy() throws Exception {
230         this.methodName = "testGLViewLargerHeightDecodeAccuracy";
231         runTest(new GLSurfaceViewFactory(), getLargerHeightVideoFormat(new VideoFormat(fileName)),
232             decoderName);
233     }
234 
235     @Test(timeout = PER_TEST_TIMEOUT_MS)
testGLViewLargerWidthDecodeAccuracy()236     public void testGLViewLargerWidthDecodeAccuracy() throws Exception {
237         this.methodName = "testGLViewLargerWidthDecodeAccuracy";
238         runTest(new GLSurfaceViewFactory(), getLargerWidthVideoFormat(new VideoFormat(fileName)),
239             decoderName);
240     }
241 
242     @Test(timeout = PER_TEST_TIMEOUT_MS)
testSurfaceViewVideoDecodeAccuracy()243     public void testSurfaceViewVideoDecodeAccuracy() throws Exception {
244         this.methodName = "testSurfaceViewVideoDecodeAccuracy";
245         runTest(new SurfaceViewFactory(), new VideoFormat(fileName), decoderName);
246     }
247 
248     @Test(timeout = PER_TEST_TIMEOUT_MS)
testSurfaceViewLargerHeightDecodeAccuracy()249     public void testSurfaceViewLargerHeightDecodeAccuracy() throws Exception {
250         this.methodName = "testSurfaceViewLargerHeightDecodeAccuracy";
251         runTest(new SurfaceViewFactory(), getLargerHeightVideoFormat(new VideoFormat(fileName)),
252             decoderName);
253     }
254 
255     @Test(timeout = PER_TEST_TIMEOUT_MS)
testSurfaceViewLargerWidthDecodeAccuracy()256     public void testSurfaceViewLargerWidthDecodeAccuracy() throws Exception {
257         this.methodName = "testSurfaceViewLargerWidthDecodeAccuracy";
258         runTest(new SurfaceViewFactory(), getLargerWidthVideoFormat(new VideoFormat(fileName)),
259             decoderName);
260     }
261 
runTest(VideoViewFactory videoViewFactory, VideoFormat vf, String decoderName)262     private void runTest(VideoViewFactory videoViewFactory, VideoFormat vf, String decoderName) {
263         Log.i(TAG, "Running test for " + vf.toPrettyString());
264         if (!MediaUtils.canDecodeVideo(vf.getMimeType(), vf.getWidth(), vf.getHeight(), 30)) {
265             MediaUtils.skipTest(TAG, "No supported codec is found.");
266             return;
267         }
268         this.videoViewFactory = checkNotNull(videoViewFactory);
269         this.videoView = videoViewFactory.createView(getHelper().getContext());
270         final int maxRetries = 3;
271         for (int retry = 1; retry <= maxRetries; retry++) {
272             // If view is intended and available to display.
273             if (videoView != null) {
274                 getHelper().generateView(videoView);
275             }
276             try {
277                 videoViewFactory.waitForViewIsAvailable();
278                 break;
279             } catch (Exception exception) {
280                 Log.e(TAG, exception.getMessage());
281                 if (retry == maxRetries) {
282                     fail("Timeout waiting for a valid surface.");
283                 } else {
284                     Log.w(TAG, "Try again...");
285                     bringActivityToFront();
286                 }
287             }
288         }
289         final int golden = getGoldenId(vf.getDescription(), vf.getOriginalSize());
290         assertTrue("No golden found.", golden != 0);
291         decodeVideo(vf, videoViewFactory, decoderName);
292         validateResult(vf, videoViewFactory.getVideoViewSnapshot(), golden, decoderName);
293     }
294 
decodeVideo(VideoFormat videoFormat, VideoViewFactory videoViewFactory, String decoderName)295     private void decodeVideo(VideoFormat videoFormat, VideoViewFactory videoViewFactory,
296             String decoderName) {
297         this.player = new SimplePlayer(getHelper().getContext(), decoderName);
298         final SimplePlayer.PlayerResult playerResult = player.decodeVideoFrames(
299                 videoViewFactory.getSurface(), videoFormat, 10);
300         assertTrue(playerResult.getFailureMessage(), playerResult.isSuccess());
301     }
302 
validateResult( VideoFormat videoFormat, VideoViewSnapshot videoViewSnapshot, int goldenId, String decoderName)303     private void validateResult(
304             VideoFormat videoFormat, VideoViewSnapshot videoViewSnapshot, int goldenId,
305             String decoderName) {
306         final Bitmap result = checkNotNull("The expected bitmap from snapshot is null",
307                 getHelper().generateBitmapFromVideoViewSnapshot(videoViewSnapshot));
308         final Bitmap golden = getHelper().generateBitmapFromImageResourceId(goldenId);
309 
310         int ignorePixels = 0;
311         if (IS_BEFORE_U && TestUtils.isTestingModules()) {
312             if (TestUtils.isMainlineCodec(decoderName)) {
313                 // some older systems don't give proper behavior at the edges (in system code).
314                 // while we can't fix the behavior at the edges, we can verify that the rest
315                 // of the image is within tolerance. b/256807044
316                 ignorePixels = 1;
317             }
318         }
319         final BitmapCompare.Difference difference = BitmapCompare.computeMinimumDifference(
320                 result, golden, ignorePixels, videoFormat.getOriginalWidth(),
321                 videoFormat.getOriginalHeight());
322 
323         if (difference.greatestPixelDifference > ALLOWED_GREATEST_PIXEL_DIFFERENCE) {
324             /* save failing file */
325             File failed = new File(Environment.getExternalStorageDirectory(),
326                                    "failed_" + methodName + "_" + testName + ".png");
327             try (FileOutputStream fileStream = new FileOutputStream(failed)) {
328                 result.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, fileStream);
329                 fileStream.flush();
330             } catch (Exception e) {
331                 e.printStackTrace();
332             }
333             Log.d(TAG, testName + " saved " + failed.getAbsolutePath());
334         }
335 
336         assertTrue("With the best matched border crop ("
337                 + difference.bestMatchBorderCrop.first + ", "
338                 + difference.bestMatchBorderCrop.second + "), "
339                 + "greatest pixel difference is "
340                 + difference.greatestPixelDifference
341                 + (difference.greatestPixelDifferenceCoordinates != null
342                         ? " at (" + difference.greatestPixelDifferenceCoordinates.first + ", "
343                             + difference.greatestPixelDifferenceCoordinates.second + ")" : "")
344                 + " which is over the allowed difference " + ALLOWED_GREATEST_PIXEL_DIFFERENCE,
345                 difference.greatestPixelDifference <= ALLOWED_GREATEST_PIXEL_DIFFERENCE);
346     }
347 
getLargerHeightVideoFormat(VideoFormat videoFormat)348     private static VideoFormat getLargerHeightVideoFormat(VideoFormat videoFormat) {
349         return new VideoFormat(videoFormat) {
350             @Override
351             public int getHeight() {
352                 return super.getHeight() + OFFSET;
353             }
354 
355             @Override
356             public boolean isAbrEnabled() {
357                 return true;
358             }
359         };
360     }
361 
362     private static VideoFormat getLargerWidthVideoFormat(VideoFormat videoFormat) {
363         return new VideoFormat(videoFormat) {
364             @Override
365             public int getWidth() {
366                 return super.getWidth() + OFFSET;
367             }
368 
369             @Override
370             public boolean isAbrEnabled() {
371                 return true;
372             }
373         };
374     }
375 
376     /**
377      * Returns the resource id by matching parts of the video and golden file name.
378      */
379     private static int getGoldenId(String description, String size) {
380         for (Field field : fields) {
381             try {
382                 final String name = field.getName();
383                 if (name.contains("golden") && name.contains(description) && name.contains(size)) {
384                     int id = field.getInt(null);
385                     return field.getInt(null);
386                 }
387             } catch (IllegalAccessException | NullPointerException e) {
388                 // No file found.
389             }
390         }
391         return 0;
392     }
393 
394 }
395