xref: /aosp_15_r20/cts/tests/tests/media/misc/src/android/media/misc/cts/HeifWriterTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2018 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.misc.cts;
18 
19 import static androidx.heifwriter.HeifWriter.INPUT_MODE_BITMAP;
20 import static androidx.heifwriter.HeifWriter.INPUT_MODE_BUFFER;
21 import static androidx.heifwriter.HeifWriter.INPUT_MODE_SURFACE;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertTrue;
25 
26 import android.content.Context;
27 import android.graphics.Bitmap;
28 import android.graphics.BitmapFactory;
29 import android.graphics.Color;
30 import android.graphics.ImageFormat;
31 import android.graphics.Rect;
32 import android.media.MediaCodecInfo;
33 import android.media.MediaCodecList;
34 import android.media.MediaExtractor;
35 import android.media.MediaFormat;
36 import android.media.MediaMetadataRetriever;
37 import android.media.cts.InputSurface;
38 import android.opengl.GLES20;
39 import android.os.Build;
40 import android.os.Environment;
41 import android.os.Handler;
42 import android.os.HandlerThread;
43 import android.os.Process;
44 import android.platform.test.annotations.AppModeFull;
45 import android.platform.test.annotations.Presubmit;
46 import android.platform.test.annotations.RequiresDevice;
47 import android.system.ErrnoException;
48 import android.system.Os;
49 import android.system.OsConstants;
50 import android.util.Log;
51 
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.heifwriter.HeifWriter;
55 import androidx.test.ext.junit.runners.AndroidJUnit4;
56 import androidx.test.filters.FlakyTest;
57 import androidx.test.filters.SmallTest;
58 import androidx.test.platform.app.InstrumentationRegistry;
59 
60 import com.android.compatibility.common.util.ApiLevelUtil;
61 import com.android.compatibility.common.util.CddTest;
62 import com.android.compatibility.common.util.MediaUtils;
63 
64 import org.junit.After;
65 import org.junit.Before;
66 import org.junit.Test;
67 import org.junit.runner.RunWith;
68 
69 import java.io.Closeable;
70 import java.io.File;
71 import java.io.FileDescriptor;
72 import java.io.FileInputStream;
73 import java.io.FileOutputStream;
74 import java.io.IOException;
75 import java.io.InputStream;
76 import java.io.OutputStream;
77 import java.io.RandomAccessFile;
78 import java.util.Arrays;
79 
80 @Presubmit
81 @SmallTest
82 @RequiresDevice
83 @AppModeFull(reason = "Instant apps cannot access the SD card")
84 @RunWith(AndroidJUnit4.class)
85 public class HeifWriterTest {
86     private static final String TAG = HeifWriterTest.class.getSimpleName();
87     private static final boolean DEBUG = false;
88     private static final boolean DUMP_OUTPUT = false;
89     private static final boolean DUMP_YUV_INPUT = false;
90     private static final int GRID_WIDTH = 512;
91     private static final int GRID_HEIGHT = 512;
92     private static final boolean IS_BEFORE_R = ApiLevelUtil.isBefore(Build.VERSION_CODES.R);
93 
94     private static byte[][] TEST_YUV_COLORS = {
95             {(byte) 255, (byte) 0, (byte) 0},
96             {(byte) 255, (byte) 0, (byte) 255},
97             {(byte) 255, (byte) 255, (byte) 255},
98             {(byte) 255, (byte) 255, (byte) 0},
99     };
100     private static Color COLOR_BLOCK =
101             Color.valueOf(1.0f, 1.0f, 1.0f);
102     private static Color[] COLOR_BARS = {
103             Color.valueOf(0.0f, 0.0f, 0.0f),
104             Color.valueOf(0.0f, 0.0f, 0.64f),
105             Color.valueOf(0.0f, 0.64f, 0.0f),
106             Color.valueOf(0.0f, 0.64f, 0.64f),
107             Color.valueOf(0.64f, 0.0f, 0.0f),
108             Color.valueOf(0.64f, 0.0f, 0.64f),
109             Color.valueOf(0.64f, 0.64f, 0.0f),
110     };
111     private static int BORDER_WIDTH = 16;
112 
113     private static final String HEIFWRITER_INPUT = "heifwriter_input.heic";
114     private static final int[] IMAGE_RESOURCES = new int[] {
115             R.raw.heifwriter_input
116     };
117     private static final String[] IMAGE_FILENAMES = new String[] {
118             HEIFWRITER_INPUT
119     };
120     private static final String OUTPUT_FILENAME = "output.heic";
121 
122     private InputSurface mInputEglSurface;
123     private Handler mHandler;
124     private int mInputIndex;
125 
getContext()126     private Context getContext() {
127         return InstrumentationRegistry.getInstrumentation().getContext();
128     }
129 
130     @Before
setUp()131     public void setUp() throws Exception {
132         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
133             String outputPath = new File(Environment.getExternalStorageDirectory(),
134                     IMAGE_FILENAMES[i]).getAbsolutePath();
135 
136             InputStream inputStream = null;
137             FileOutputStream outputStream = null;
138             try {
139                 inputStream = getContext().getResources().openRawResource(IMAGE_RESOURCES[i]);
140                 outputStream = new FileOutputStream(outputPath);
141                 copy(inputStream, outputStream);
142             } finally {
143                 closeQuietly(inputStream);
144                 closeQuietly(outputStream);
145             }
146         }
147 
148         HandlerThread handlerThread = new HandlerThread(
149                 "HeifEncoderThread", Process.THREAD_PRIORITY_FOREGROUND);
150         handlerThread.start();
151         mHandler = new Handler(handlerThread.getLooper());
152     }
153 
154     @After
tearDown()155     public void tearDown() throws Exception {
156         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
157             String imageFilePath =
158                     new File(Environment.getExternalStorageDirectory(), IMAGE_FILENAMES[i])
159                             .getAbsolutePath();
160             File imageFile = new File(imageFilePath);
161             if (imageFile.exists()) {
162                 imageFile.delete();
163             }
164         }
165     }
166 
167     @Test
testInputBuffer_NoGrid_NoHandler()168     public void testInputBuffer_NoGrid_NoHandler() throws Throwable {
169         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, false, false);
170         doTestForVariousNumberImages(builder);
171     }
172 
173     @Test
testInputBuffer_Grid_NoHandler()174     public void testInputBuffer_Grid_NoHandler() throws Throwable {
175         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, true, false);
176         doTestForVariousNumberImages(builder);
177     }
178 
179     @Test
testInputBuffer_NoGrid_Handler()180     public void testInputBuffer_NoGrid_Handler() throws Throwable {
181         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, false, true);
182         doTestForVariousNumberImages(builder);
183     }
184 
185     @Test
testInputBuffer_Grid_Handler()186     public void testInputBuffer_Grid_Handler() throws Throwable {
187         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BUFFER, true, true);
188         doTestForVariousNumberImages(builder);
189     }
190 
191     // TODO: b/186001256
192     @Test
193     @FlakyTest
testInputSurface_NoGrid_NoHandler()194     public void testInputSurface_NoGrid_NoHandler() throws Throwable {
195         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, false, false);
196         doTestForVariousNumberImages(builder);
197     }
198 
199     @Test
200     @FlakyTest
testInputSurface_Grid_NoHandler()201     public void testInputSurface_Grid_NoHandler() throws Throwable {
202         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, true, false);
203         doTestForVariousNumberImages(builder);
204     }
205 
206     @Test
207     @FlakyTest
testInputSurface_NoGrid_Handler()208     public void testInputSurface_NoGrid_Handler() throws Throwable {
209         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, false, true);
210         doTestForVariousNumberImages(builder);
211     }
212 
213     @Test
214     @FlakyTest
testInputSurface_Grid_Handler()215     public void testInputSurface_Grid_Handler() throws Throwable {
216         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_SURFACE, true, true);
217         doTestForVariousNumberImages(builder);
218     }
219 
220     @Test
testInputBitmap_NoGrid_NoHandler()221     public void testInputBitmap_NoGrid_NoHandler() throws Throwable {
222         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, false, false);
223         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
224             String inputPath = new File(Environment.getExternalStorageDirectory(),
225                     IMAGE_FILENAMES[i]).getAbsolutePath();
226             doTestForVariousNumberImages(builder.setInputPath(inputPath));
227         }
228     }
229 
230     @Test
testInputBitmap_Grid_NoHandler()231     public void testInputBitmap_Grid_NoHandler() throws Throwable {
232         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, true, false);
233         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
234             String inputPath = new File(Environment.getExternalStorageDirectory(),
235                     IMAGE_FILENAMES[i]).getAbsolutePath();
236             doTestForVariousNumberImages(builder.setInputPath(inputPath));
237         }
238     }
239 
240     @Test
testInputBitmap_NoGrid_Handler()241     public void testInputBitmap_NoGrid_Handler() throws Throwable {
242         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, false, true);
243         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
244             String inputPath = new File(Environment.getExternalStorageDirectory(),
245                     IMAGE_FILENAMES[i]).getAbsolutePath();
246             doTestForVariousNumberImages(builder.setInputPath(inputPath));
247         }
248     }
249 
250     @Test
testInputBitmap_Grid_Handler()251     public void testInputBitmap_Grid_Handler() throws Throwable {
252         TestConfig.Builder builder = new TestConfig.Builder(INPUT_MODE_BITMAP, true, true);
253         for (int i = 0; i < IMAGE_RESOURCES.length; ++i) {
254             String inputPath = new File(Environment.getExternalStorageDirectory(),
255                     IMAGE_FILENAMES[i]).getAbsolutePath();
256             doTestForVariousNumberImages(builder.setInputPath(inputPath));
257         }
258     }
259 
260     /**
261      * This test is to ensure that if the device advertises support for {@link
262      * MediaFormat#MIMETYPE_IMAGE_ANDROID_HEIC} (which encodes full-frame image
263      * with tiling), it must also support {@link MediaFormat#MIMETYPE_VIDEO_HEVC}
264      * at a specific tile size (512x512) with bitrate control mode {@link
265      * MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CQ}, so that a fallback
266      * could be implemented for image resolutions that's not supported by the
267      * {@link MediaFormat#MIMETYPE_IMAGE_ANDROID_HEIC} encoder.
268      */
269     @CddTest(requirement="5.1.4/C-1-1")
270     @Test
testHeicFallbackAvailable()271     public void testHeicFallbackAvailable() throws Throwable {
272         if (!MediaUtils.hasEncoder(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) {
273             MediaUtils.skipTest("HEIC full-frame image encoder is not supported on this device");
274             return;
275         }
276 
277         final MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
278 
279         boolean fallbackFound = false;
280         for (MediaCodecInfo info : mcl.getCodecInfos()) {
281             if (!info.isEncoder() || !info.isHardwareAccelerated()) {
282                 continue;
283             }
284             MediaCodecInfo.CodecCapabilities caps = null;
285             try {
286                 caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_HEVC);
287             } catch (IllegalArgumentException e) { // mime is not supported
288                 continue;
289             }
290             if (caps.getVideoCapabilities().isSizeSupported(GRID_WIDTH, GRID_HEIGHT) &&
291                     caps.getEncoderCapabilities().isBitrateModeSupported(
292                             MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ)) {
293                 fallbackFound = true;
294                 Log.d(TAG, "found fallback on " + info.getName());
295                 // not breaking here so that we can log what's available by running this test
296             }
297         }
298         assertTrue("HEIC full-frame image encoder without HEVC fallback", fallbackFound);
299     }
300 
canEncodeHeic()301     private static boolean canEncodeHeic() {
302         return MediaUtils.hasEncoder(MediaFormat.MIMETYPE_VIDEO_HEVC)
303             || MediaUtils.hasEncoder(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC);
304     }
305 
doTestForVariousNumberImages(TestConfig.Builder builder)306     private void doTestForVariousNumberImages(TestConfig.Builder builder) throws Exception {
307         if (!canEncodeHeic()) {
308             MediaUtils.skipTest("heic encoding is not supported on this device");
309             return;
310         }
311 
312         builder.setNumImages(4);
313         doTest(builder.setRotation(270).build());
314         doTest(builder.setRotation(180).build());
315         doTest(builder.setRotation(90).build());
316         doTest(builder.setRotation(0).build());
317         doTest(builder.setNumImages(1).build());
318         doTest(builder.setNumImages(8).build());
319     }
320 
closeQuietly(Closeable closeable)321     private void closeQuietly(Closeable closeable) {
322         if (closeable != null) {
323             try {
324                 closeable.close();
325             } catch (RuntimeException rethrown) {
326                 throw rethrown;
327             } catch (Exception ignored) {
328             }
329         }
330     }
331 
copy(InputStream in, OutputStream out)332     private int copy(InputStream in, OutputStream out) throws IOException {
333         int total = 0;
334         byte[] buffer = new byte[8192];
335         int c;
336         while ((c = in.read(buffer)) != -1) {
337             total += c;
338             out.write(buffer, 0, c);
339         }
340         return total;
341     }
342 
343     private static class TestConfig {
344         final int mInputMode;
345         final boolean mUseGrid;
346         final boolean mUseHandler;
347         final int mMaxNumImages;
348         final int mActualNumImages;
349         final int mWidth;
350         final int mHeight;
351         final int mRotation;
352         final int mQuality;
353         final String mInputPath;
354         final String mOutputPath;
355         final Bitmap[] mBitmaps;
356 
TestConfig(int inputMode, boolean useGrid, boolean useHandler, int maxNumImages, int actualNumImages, int width, int height, int rotation, int quality, String inputPath, String outputPath, Bitmap[] bitmaps)357         TestConfig(int inputMode, boolean useGrid, boolean useHandler,
358                    int maxNumImages, int actualNumImages, int width, int height,
359                    int rotation, int quality,
360                    String inputPath, String outputPath, Bitmap[] bitmaps) {
361             mInputMode = inputMode;
362             mUseGrid = useGrid;
363             mUseHandler = useHandler;
364             mMaxNumImages = maxNumImages;
365             mActualNumImages = actualNumImages;
366             mWidth = width;
367             mHeight = height;
368             mRotation = rotation;
369             mQuality = quality;
370             mInputPath = inputPath;
371             mOutputPath = outputPath;
372             mBitmaps = bitmaps;
373         }
374 
375         static class Builder {
376             final int mInputMode;
377             final boolean mUseGrid;
378             final boolean mUseHandler;
379             int mMaxNumImages;
380             int mNumImages;
381             int mWidth;
382             int mHeight;
383             int mRotation;
384             final int mQuality;
385             String mInputPath;
386             final String mOutputPath;
387             Bitmap[] mBitmaps;
388             boolean mNumImagesSetExplicitly;
389 
390 
Builder(int inputMode, boolean useGrids, boolean useHandler)391             Builder(int inputMode, boolean useGrids, boolean useHandler) {
392                 mInputMode = inputMode;
393                 mUseGrid = useGrids;
394                 mUseHandler = useHandler;
395                 mMaxNumImages = mNumImages = 4;
396                 mWidth = 1920;
397                 mHeight = 1080;
398                 mRotation = 0;
399                 mQuality = 100;
400                 // use memfd by default
401                 if (DUMP_OUTPUT || IS_BEFORE_R) {
402                     mOutputPath = new File(Environment.getExternalStorageDirectory(),
403                             OUTPUT_FILENAME).getAbsolutePath();
404                 } else {
405                     mOutputPath = null;
406                 }
407             }
408 
setInputPath(String inputPath)409             Builder setInputPath(String inputPath) {
410                 mInputPath = (mInputMode == INPUT_MODE_BITMAP) ? inputPath : null;
411                 return this;
412             }
413 
setNumImages(int numImages)414             Builder setNumImages(int numImages) {
415                 mNumImagesSetExplicitly = true;
416                 mNumImages = numImages;
417                 return this;
418             }
419 
setRotation(int rotation)420             Builder setRotation(int rotation) {
421                 mRotation = rotation;
422                 return this;
423             }
424 
loadBitmapInputs()425             private void loadBitmapInputs() {
426                 if (mInputMode != INPUT_MODE_BITMAP) {
427                     return;
428                 }
429                 MediaMetadataRetriever retriever = new MediaMetadataRetriever();
430                 retriever.setDataSource(mInputPath);
431                 String hasImage = retriever.extractMetadata(
432                         MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
433                 if (!"yes".equals(hasImage)) {
434                     throw new IllegalArgumentException("no bitmap found!");
435                 }
436                 mMaxNumImages = Math.min(mMaxNumImages, Integer.parseInt(retriever.extractMetadata(
437                         MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
438                 if (!mNumImagesSetExplicitly) {
439                     mNumImages = mMaxNumImages;
440                 }
441                 mBitmaps = new Bitmap[mMaxNumImages];
442                 for (int i = 0; i < mBitmaps.length; i++) {
443                     mBitmaps[i] = retriever.getImageAtIndex(i);
444                 }
445                 mWidth = mBitmaps[0].getWidth();
446                 mHeight = mBitmaps[0].getHeight();
447                 try {
448                     retriever.release();
449                 } catch (IOException e) {
450                     // Ignoring errors occurred while releasing the MediaMetadataRetriever.
451                 }
452             }
453 
cleanupStaleOutputs()454             private void cleanupStaleOutputs() {
455                 if (mOutputPath != null) {
456                     File outputFile = new File(mOutputPath);
457                     if (outputFile.exists()) {
458                         outputFile.delete();
459                     }
460                 }
461             }
462 
build()463             TestConfig build() {
464                 cleanupStaleOutputs();
465                 loadBitmapInputs();
466 
467                 return new TestConfig(mInputMode, mUseGrid, mUseHandler, mMaxNumImages, mNumImages,
468                         mWidth, mHeight, mRotation, mQuality, mInputPath, mOutputPath, mBitmaps);
469             }
470         }
471 
472         @Override
toString()473         public String toString() {
474             return "TestConfig"
475                     + ": mInputMode " + mInputMode
476                     + ", mUseGrid " + mUseGrid
477                     + ", mUseHandler " + mUseHandler
478                     + ", mMaxNumImages " + mMaxNumImages
479                     + ", mNumImages " + mActualNumImages
480                     + ", mWidth " + mWidth
481                     + ", mHeight " + mHeight
482                     + ", mRotation " + mRotation
483                     + ", mQuality " + mQuality
484                     + ", mInputPath " + mInputPath
485                     + ", mOutputPath " + mOutputPath;
486         }
487     }
488 
doTest(final TestConfig config)489     private void doTest(final TestConfig config) throws Exception {
490         final int width = config.mWidth;
491         final int height = config.mHeight;
492         final int actualNumImages = config.mActualNumImages;
493 
494         mInputIndex = 0;
495         HeifWriter heifWriter = null;
496         FileInputStream inputYuvStream = null;
497         FileOutputStream outputYuvStream = null;
498         FileDescriptor outputFd = null;
499         RandomAccessFile outputFile = null;
500         try {
501             if (DEBUG) Log.d(TAG, "started: " + config);
502             if (config.mOutputPath != null) {
503                 outputFile = new RandomAccessFile(config.mOutputPath, "rws");
504                 outputFile.setLength(0);
505                 outputFd = outputFile.getFD();
506             } else {
507                 outputFd = Os.memfd_create("temp", OsConstants.MFD_CLOEXEC);
508             }
509 
510             heifWriter = new HeifWriter.Builder(
511                     outputFd, width, height, config.mInputMode)
512                     .setRotation(config.mRotation)
513                     .setGridEnabled(config.mUseGrid)
514                     .setMaxImages(config.mMaxNumImages)
515                     .setQuality(config.mQuality)
516                     .setPrimaryIndex(config.mMaxNumImages - 1)
517                     .setHandler(config.mUseHandler ? mHandler : null)
518                     .build();
519 
520             if (config.mInputMode == INPUT_MODE_SURFACE) {
521                 mInputEglSurface = new InputSurface(heifWriter.getInputSurface());
522             }
523 
524             heifWriter.start();
525 
526             if (config.mInputMode == INPUT_MODE_BUFFER) {
527                 byte[] data = new byte[width * height * 3 / 2];
528 
529                 if (config.mInputPath != null) {
530                     inputYuvStream = new FileInputStream(config.mInputPath);
531                 }
532 
533                 if (DUMP_YUV_INPUT) {
534                     File outputYuvFile = new File("/sdcard/input.yuv");
535                     outputYuvFile.createNewFile();
536                     outputYuvStream = new FileOutputStream(outputYuvFile);
537                 }
538 
539                 for (int i = 0; i < actualNumImages; i++) {
540                     if (DEBUG) Log.d(TAG, "fillYuvBuffer: " + i);
541                     fillYuvBuffer(i, data, width, height, inputYuvStream);
542                     if (DUMP_YUV_INPUT) {
543                         Log.d(TAG, "@@@ dumping input YUV");
544                         outputYuvStream.write(data);
545                     }
546                     heifWriter.addYuvBuffer(ImageFormat.YUV_420_888, data);
547                 }
548             } else if (config.mInputMode == INPUT_MODE_SURFACE) {
549                 // The input surface is a surface texture using single buffer mode, draws will be
550                 // blocked until onFrameAvailable is done with the buffer, which is dependent on
551                 // how fast MediaCodec processes them, which is further dependent on how fast the
552                 // MediaCodec callbacks are handled. We can't put draws on the same looper that
553                 // handles MediaCodec callback, it will cause deadlock.
554                 for (int i = 0; i < actualNumImages; i++) {
555                     if (DEBUG) Log.d(TAG, "drawFrame: " + i);
556                     drawFrame(width, height);
557                 }
558                 heifWriter.setInputEndOfStreamTimestamp(
559                         1000 * computePresentationTime(actualNumImages - 1));
560             } else if (config.mInputMode == INPUT_MODE_BITMAP) {
561                 Bitmap[] bitmaps = config.mBitmaps;
562                 for (int i = 0; i < Math.min(bitmaps.length, actualNumImages); i++) {
563                     if (DEBUG) Log.d(TAG, "addBitmap: " + i);
564                     heifWriter.addBitmap(bitmaps[i]);
565                     bitmaps[i].recycle();
566                 }
567             }
568 
569             heifWriter.stop(5000);
570             // The test sets the primary index to the last image.
571             // However, if we're testing early abort, the last image will not be
572             // present and the muxer is supposed to set it to 0 by default.
573             int expectedPrimary = config.mMaxNumImages - 1;
574             int expectedImageCount = config.mMaxNumImages;
575             if (actualNumImages < config.mMaxNumImages) {
576                 expectedPrimary = 0;
577                 expectedImageCount = actualNumImages;
578             }
579             verifyResult(outputFd, width, height, config.mRotation,
580                     expectedImageCount, expectedPrimary, config.mUseGrid,
581                     config.mInputMode == INPUT_MODE_SURFACE);
582             if (DEBUG) Log.d(TAG, "finished: PASS");
583         } finally {
584             try {
585                 if (outputYuvStream != null) {
586                     outputYuvStream.close();
587                 }
588                 if (inputYuvStream != null) {
589                     inputYuvStream.close();
590                 }
591                 if (outputFile != null) {
592                     outputFile.close();
593                 }
594                 if (outputFd != null) {
595                     Os.close(outputFd);
596                 }
597             } catch (IOException|ErrnoException e) {}
598 
599             if (heifWriter != null) {
600                 heifWriter.close();
601                 heifWriter = null;
602             }
603             if (mInputEglSurface != null) {
604                 // This also releases the surface from encoder.
605                 mInputEglSurface.release();
606                 mInputEglSurface = null;
607             }
608         }
609     }
610 
computePresentationTime(int frameIndex)611     private long computePresentationTime(int frameIndex) {
612         return 132 + (long)frameIndex * 1000000;
613     }
614 
fillYuvBuffer(int frameIndex, @NonNull byte[] data, int width, int height, @Nullable FileInputStream inputStream)615     private void fillYuvBuffer(int frameIndex, @NonNull byte[] data, int width, int height,
616                                @Nullable FileInputStream inputStream) throws IOException {
617         if (inputStream != null) {
618             inputStream.read(data);
619         } else {
620             byte[] color = TEST_YUV_COLORS[frameIndex % TEST_YUV_COLORS.length];
621             int sizeY = width * height;
622             Arrays.fill(data, 0, sizeY, color[0]);
623             Arrays.fill(data, sizeY, sizeY * 5 / 4, color[1]);
624             Arrays.fill(data, sizeY * 5 / 4, sizeY * 3 / 2, color[2]);
625         }
626     }
627 
drawFrame(int width, int height)628     private void drawFrame(int width, int height) {
629         mInputEglSurface.makeCurrent();
630         generateSurfaceFrame(mInputIndex, width, height);
631         mInputEglSurface.setPresentationTime(1000 * computePresentationTime(mInputIndex));
632         mInputEglSurface.swapBuffers();
633         mInputIndex++;
634     }
635 
getColorBarRect(int index, int width, int height)636     private static Rect getColorBarRect(int index, int width, int height) {
637         int barWidth = (width - BORDER_WIDTH * 2) / COLOR_BARS.length;
638         return new Rect(BORDER_WIDTH + barWidth * index, BORDER_WIDTH,
639                 BORDER_WIDTH + barWidth * (index + 1), height - BORDER_WIDTH);
640     }
641 
getColorBlockRect(int index, int width, int height)642     private static Rect getColorBlockRect(int index, int width, int height) {
643         int blockCenterX = (width / 5) * (index % 4 + 1);
644         return new Rect(blockCenterX - width / 10, height / 6,
645                         blockCenterX + width / 10, height / 3);
646     }
647 
generateSurfaceFrame(int frameIndex, int width, int height)648     private void generateSurfaceFrame(int frameIndex, int width, int height) {
649         GLES20.glViewport(0, 0, width, height);
650         GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
651         GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
652         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
653         GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
654 
655         for (int i = 0; i < COLOR_BARS.length; i++) {
656             Rect r = getColorBarRect(i, width, height);
657 
658             GLES20.glScissor(r.left, r.top, r.width(), r.height());
659             final Color color = COLOR_BARS[i];
660             GLES20.glClearColor(color.red(), color.green(), color.blue(), 1.0f);
661             GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
662         }
663 
664         Rect r = getColorBlockRect(frameIndex, width, height);
665         GLES20.glScissor(r.left, r.top, r.width(), r.height());
666         GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
667         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
668         r.inset(BORDER_WIDTH, BORDER_WIDTH);
669         GLES20.glScissor(r.left, r.top, r.width(), r.height());
670         GLES20.glClearColor(COLOR_BLOCK.red(), COLOR_BLOCK.green(), COLOR_BLOCK.blue(), 1.0f);
671         GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
672     }
673 
674     /**
675      * Determines if two color values are approximately equal.
676      */
approxEquals(Color expected, Color actual)677     private static boolean approxEquals(Color expected, Color actual) {
678         final float MAX_DELTA = 0.025f;
679         return (Math.abs(expected.red() - actual.red()) <= MAX_DELTA)
680             && (Math.abs(expected.green() - actual.green()) <= MAX_DELTA)
681             && (Math.abs(expected.blue() - actual.blue()) <= MAX_DELTA);
682     }
683 
verifyResult( FileDescriptor fd, int width, int height, int rotation, int imageCount, int primary, boolean useGrid, boolean checkColor)684     private void verifyResult(
685             FileDescriptor fd, int width, int height, int rotation,
686             int imageCount, int primary, boolean useGrid, boolean checkColor)
687             throws Exception {
688         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
689         retriever.setDataSource(fd);
690         String hasImage = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
691         if (!"yes".equals(hasImage)) {
692             throw new Exception("No images found in file descriptor");
693         }
694         assertEquals("Wrong width", width,
695                 Integer.parseInt(retriever.extractMetadata(
696                     MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH)));
697         assertEquals("Wrong height", height,
698                 Integer.parseInt(retriever.extractMetadata(
699                     MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)));
700         assertEquals("Wrong rotation", rotation,
701                 Integer.parseInt(retriever.extractMetadata(
702                     MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION)));
703         assertEquals("Wrong image count", imageCount,
704                 Integer.parseInt(retriever.extractMetadata(
705                         MediaMetadataRetriever.METADATA_KEY_IMAGE_COUNT)));
706         assertEquals("Wrong primary index", primary,
707                 Integer.parseInt(retriever.extractMetadata(
708                         MediaMetadataRetriever.METADATA_KEY_IMAGE_PRIMARY)));
709         retriever.release();
710 
711         if (useGrid) {
712             MediaExtractor extractor = new MediaExtractor();
713             extractor.setDataSource(fd);
714             MediaFormat format = extractor.getTrackFormat(0);
715             int tileWidth = format.getInteger(MediaFormat.KEY_TILE_WIDTH);
716             int tileHeight = format.getInteger(MediaFormat.KEY_TILE_HEIGHT);
717             int gridRows = format.getInteger(MediaFormat.KEY_GRID_ROWS);
718             int gridCols = format.getInteger(MediaFormat.KEY_GRID_COLUMNS);
719             assertTrue("Wrong tile width or grid cols",
720                     ((width + tileWidth - 1) / tileWidth) == gridCols);
721             assertTrue("Wrong tile height or grid rows",
722                     ((height + tileHeight - 1) / tileHeight) == gridRows);
723             extractor.release();
724         }
725 
726         if (checkColor) {
727             Os.lseek(fd, 0, OsConstants.SEEK_SET);
728             Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
729 
730             for (int i = 0; i < COLOR_BARS.length; i++) {
731                 Rect r = getColorBarRect(i, width, height);
732                 assertTrue("Color bar " + i + " doesn't match", approxEquals(COLOR_BARS[i],
733                         Color.valueOf(bitmap.getPixel(r.centerX(), r.centerY()))));
734             }
735 
736             Rect r = getColorBlockRect(primary, width, height);
737             assertTrue("Color block doesn't match", approxEquals(COLOR_BLOCK,
738                     Color.valueOf(bitmap.getPixel(r.centerX(), height - r.centerY()))));
739         }
740     }
741 }
742