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