Home | History | Annotate | Download | only in cts
      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.cts;
     17 
     18 import android.media.cts.R;
     19 
     20 import static org.junit.Assert.assertNotNull;
     21 
     22 import com.android.compatibility.common.util.ApiLevelUtil;
     23 import com.android.compatibility.common.util.MediaUtils;
     24 
     25 import android.annotation.TargetApi;
     26 import android.annotation.SuppressLint;
     27 import android.app.Activity;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.pm.ActivityInfo;
     31 import android.content.res.AssetFileDescriptor;
     32 import android.content.res.Configuration;
     33 import android.content.res.Resources;
     34 import android.graphics.Bitmap;
     35 import android.graphics.Bitmap.Config;
     36 import android.graphics.BitmapFactory;
     37 import android.graphics.Color;
     38 import android.graphics.SurfaceTexture;
     39 import android.media.MediaCodec;
     40 import android.media.MediaCodec.BufferInfo;
     41 import android.media.MediaCodec.CodecException;
     42 import android.media.MediaCodecInfo.VideoCapabilities;
     43 import android.media.MediaCodecList;
     44 import android.media.MediaExtractor;
     45 import android.media.MediaFormat;
     46 import android.net.Uri;
     47 import android.opengl.EGL14;
     48 import android.opengl.GLES11Ext;
     49 import android.opengl.GLES20;
     50 import android.opengl.GLSurfaceView;
     51 import android.os.Build;
     52 import android.os.Handler;
     53 import android.os.HandlerThread;
     54 import android.os.Looper;
     55 import android.os.SystemClock;
     56 import android.support.test.rule.ActivityTestRule;
     57 import android.util.Log;
     58 import android.util.Pair;
     59 import android.util.SparseArray;
     60 import android.view.PixelCopy;
     61 import android.view.PixelCopy.OnPixelCopyFinishedListener;
     62 import android.view.Surface;
     63 import android.view.SurfaceHolder;
     64 import android.view.SurfaceView;
     65 import android.view.TextureView;
     66 import android.view.View;
     67 import android.view.ViewGroup;
     68 import android.widget.RelativeLayout;
     69 
     70 import java.io.File;
     71 import java.io.IOException;
     72 import java.nio.ByteBuffer;
     73 import java.nio.ByteOrder;
     74 import java.nio.FloatBuffer;
     75 import java.util.concurrent.TimeUnit;
     76 import java.util.HashMap;
     77 
     78 import javax.microedition.khronos.egl.EGL10;
     79 import javax.microedition.khronos.egl.EGLConfig;
     80 import javax.microedition.khronos.egl.EGLContext;
     81 import javax.microedition.khronos.egl.EGLDisplay;
     82 import javax.microedition.khronos.egl.EGLSurface;
     83 
     84 import org.junit.After;
     85 import org.junit.Before;
     86 import org.junit.Rule;
     87 
     88 @TargetApi(16)
     89 public class DecodeAccuracyTestBase {
     90 
     91     protected Context mContext;
     92     protected Resources mResources;
     93     protected DecodeAccuracyTestActivity mActivity;
     94     protected TestHelper testHelper;
     95 
     96     @Rule
     97     public ActivityTestRule<DecodeAccuracyTestActivity> mActivityRule =
     98             new ActivityTestRule<>(DecodeAccuracyTestActivity.class);
     99 
    100     @Before
    101     public void setUp() throws Exception {
    102         mActivity = mActivityRule.getActivity();
    103         mContext = mActivity.getApplicationContext();
    104         mResources = mActivity.getResources();
    105         testHelper = new TestHelper(mContext, mActivity);
    106     }
    107 
    108     @After
    109     public void tearDown() throws Exception {
    110         mActivity = null;
    111         mResources = null;
    112         mContext = null;
    113         mActivityRule = null;
    114     }
    115 
    116     protected void bringActivityToFront() {
    117         Intent intent = new Intent(mContext, DecodeAccuracyTestActivity.class);
    118         intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
    119         mActivity.startActivity(intent);
    120     }
    121 
    122     protected TestHelper getHelper() {
    123         return testHelper;
    124     }
    125 
    126     public static <T> T checkNotNull(T reference) {
    127         assertNotNull(reference);
    128         return reference;
    129     }
    130 
    131     public static <T> T checkNotNull(String msg, T reference) {
    132         assertNotNull(msg, reference);
    133         return reference;
    134     }
    135 
    136     /* Simple Player that decodes a local video file only. */
    137     @TargetApi(16)
    138     static class SimplePlayer {
    139 
    140         public static final long MIN_MS_PER_FRAME = TimeUnit.SECONDS.toMillis(1) / 5; // 5 FPS
    141         public static final long STARTUP_ALLOW_MS = TimeUnit.SECONDS.toMillis(1) ;
    142         public static final int END_OF_STREAM = -1;
    143         public static final int DEQUEUE_SUCCESS = 1;
    144         public static final int DEQUEUE_FAIL = 0;
    145 
    146         private static final String TAG = SimplePlayer.class.getSimpleName();
    147         private static final int NO_TRACK_INDEX = -3;
    148         private static final long DEQUEUE_TIMEOUT_US = 20;
    149 
    150         private final Context context;
    151         private final MediaExtractor extractor;
    152         private final String codecName;
    153         private MediaCodec decoder;
    154         private byte[] outputBytes;
    155         private boolean renderToSurface;
    156         private MediaCodecList mediaCodecList;
    157         private Surface surface;
    158 
    159         public SimplePlayer(Context context) {
    160             this(context, null);
    161         }
    162 
    163         public SimplePlayer(Context context, String codecName) {
    164             this.context = checkNotNull(context);
    165             this.codecName = codecName;
    166             this.extractor = new MediaExtractor();
    167             this.renderToSurface = false;
    168             this.surface = null;
    169         }
    170 
    171         /**
    172          * The function play the corresponding file for certain number of frames.
    173          *
    174          * @param surface is the surface view of decoder output.
    175          * @param videoFormat is the format of the video to extract and decode.
    176          * @param numOfTotalFrames is the number of Frame wish to play.
    177          * @param msPerFrameCap is the maximum msec per frame. No cap is set if value is less than 1.
    178          * @return {@link PlayerResult} that consists the result.
    179          */
    180         public PlayerResult decodeVideoFrames(
    181                 Surface surface, VideoFormat videoFormat, int numOfTotalFrames, long msPerFrameCap,
    182                 boolean releasePlayer) {
    183             this.surface = surface;
    184             PlayerResult playerResult;
    185             if (prepareVideoDecode(videoFormat)) {
    186                 if (startDecoder()) {
    187                     final long timeout =
    188                             Math.max(MIN_MS_PER_FRAME, msPerFrameCap) * numOfTotalFrames + STARTUP_ALLOW_MS;
    189                     playerResult = decodeFramesAndPlay(numOfTotalFrames, timeout, msPerFrameCap);
    190                 } else {
    191                     playerResult = PlayerResult.failToStart();
    192                 }
    193             } else {
    194                 playerResult = new PlayerResult();
    195             }
    196             if (releasePlayer) {
    197                 release();
    198             }
    199             return new PlayerResult(playerResult);
    200         }
    201 
    202         public PlayerResult decodeVideoFrames(
    203                 Surface surface, VideoFormat videoFormat, int numOfTotalFrames) {
    204             return decodeVideoFrames(surface, videoFormat, numOfTotalFrames, 0, false);
    205         }
    206 
    207         /**
    208          * The function sets up the extractor and video decoder with proper format.
    209          * This must be called before doing starting up the decoder.
    210          */
    211         private boolean prepareVideoDecode(VideoFormat videoFormat) {
    212             MediaFormat mediaFormat = prepareExtractor(videoFormat);
    213             if (mediaFormat == null) {
    214                 return false;
    215             }
    216             configureVideoFormat(mediaFormat, videoFormat);
    217             setRenderToSurface(surface != null);
    218             return createDecoder(mediaFormat) && configureDecoder(surface, mediaFormat);
    219         }
    220 
    221         /**
    222          * Sets up the extractor and gets the {@link MediaFormat} of the track.
    223          */
    224         private MediaFormat prepareExtractor(VideoFormat videoFormat) {
    225             if (!setExtractorDataSource(videoFormat)) {
    226                 return null;
    227             }
    228             final int trackNum = getFirstTrackIndexByType(videoFormat.getMediaFormat());
    229             if (trackNum == NO_TRACK_INDEX) {
    230                 return null;
    231             }
    232             extractor.selectTrack(trackNum);
    233             return extractor.getTrackFormat(trackNum);
    234         }
    235 
    236         /**
    237          * The function decode video frames and display in a surface.
    238          *
    239          * @param numOfTotalFrames is the number of frames to be decoded.
    240          * @param timeOutMs is the time limit for decoding the frames.
    241          * @param msPerFrameCap is the maximum msec per frame. No cap is set if value is less than 1.
    242          * @return {@link PlayerResult} that consists the result.
    243          */
    244         private PlayerResult decodeFramesAndPlay(
    245                 int numOfTotalFrames, long timeOutMs, long msPerFrameCap) {
    246             int numOfDecodedFrames = 0;
    247             long firstOutputTimeMs = 0;
    248             long lastFrameAt = 0;
    249             final long loopStart = SystemClock.elapsedRealtime();
    250 
    251             while (numOfDecodedFrames < numOfTotalFrames
    252                     && (SystemClock.elapsedRealtime() - loopStart < timeOutMs)) {
    253                 try {
    254                     queueDecoderInputBuffer();
    255                 } catch (IllegalStateException exception) {
    256                     Log.e(TAG, "IllegalStateException in queueDecoderInputBuffer", exception);
    257                     break;
    258                 }
    259                 try {
    260                     final int outputResult = dequeueDecoderOutputBuffer();
    261                     if (outputResult == SimplePlayer.END_OF_STREAM) {
    262                         break;
    263                     }
    264                     if (outputResult == SimplePlayer.DEQUEUE_SUCCESS) {
    265                         if (firstOutputTimeMs == 0) {
    266                             firstOutputTimeMs = SystemClock.elapsedRealtime();
    267                         }
    268                         if (msPerFrameCap > 0) {
    269                             // Slow down if cap is set and not reached.
    270                             final long delayMs =
    271                                     msPerFrameCap - (SystemClock.elapsedRealtime() - lastFrameAt);
    272                             if (lastFrameAt != 0 && delayMs > 0) {
    273                                 final long threadDelayMs = 3; // In case of delay in thread.
    274                                 if (delayMs > threadDelayMs) {
    275                                     try {
    276                                         Thread.sleep(delayMs - threadDelayMs);
    277                                     } catch (InterruptedException ex) { /* */}
    278                                 }
    279                                 while (SystemClock.elapsedRealtime() - lastFrameAt
    280                                         < msPerFrameCap) { /* */ }
    281                             }
    282                             lastFrameAt = SystemClock.elapsedRealtime();
    283                         }
    284                         numOfDecodedFrames++;
    285                     }
    286                 } catch (IllegalStateException exception) {
    287                     Log.e(TAG, "IllegalStateException in dequeueDecoderOutputBuffer", exception);
    288                 }
    289             }
    290             // NB: totalTime measures from "first output" instead of
    291             // "first INPUT", so does not include first frame latency
    292             // and therefore does not tell us if the timeout expired
    293             final long totalTime = SystemClock.elapsedRealtime() - firstOutputTimeMs;
    294             return new PlayerResult(true, true, numOfTotalFrames == numOfDecodedFrames, totalTime);
    295         }
    296 
    297         /**
    298          * Queues the input buffer with the media file one buffer at a time.
    299          *
    300          * @return true if success, fail otherwise.
    301          */
    302         private boolean queueDecoderInputBuffer() {
    303             ByteBuffer inputBuffer;
    304             final ByteBuffer[] inputBufferArray = decoder.getInputBuffers();
    305             final int inputBufferIndex = decoder.dequeueInputBuffer(DEQUEUE_TIMEOUT_US);
    306             if (inputBufferIndex >= 0) {
    307                 if (ApiLevelUtil.isBefore(Build.VERSION_CODES.LOLLIPOP)) {
    308                     inputBuffer = inputBufferArray[inputBufferIndex];
    309                 } else {
    310                     inputBuffer = decoder.getInputBuffer(inputBufferIndex);
    311                 }
    312                 final int sampleSize = extractor.readSampleData(inputBuffer, 0);
    313                 if (sampleSize > 0) {
    314                     decoder.queueInputBuffer(
    315                             inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0);
    316                     extractor.advance();
    317                 }
    318                 return true;
    319             }
    320             return false;
    321         }
    322 
    323         /**
    324          * Dequeues the output buffer.
    325          * For video decoder, renders to surface if provided.
    326          * For audio decoder, gets the bytes from the output buffer.
    327          *
    328          * @return an integer indicating its status (fail, success, or end of stream).
    329          */
    330         private int dequeueDecoderOutputBuffer() {
    331             final BufferInfo info = new BufferInfo();
    332             final int decoderStatus = decoder.dequeueOutputBuffer(info, DEQUEUE_TIMEOUT_US);
    333             if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    334                 return END_OF_STREAM;
    335             }
    336             if (decoderStatus >= 0) {
    337                 // For JELLY_BEAN_MR2- devices, when rendering to a surface,
    338                 // info.size seems to always return 0 even if
    339                 // the decoder successfully decoded the frame.
    340                 if (info.size <= 0 && ApiLevelUtil.isAtLeast(Build.VERSION_CODES.JELLY_BEAN_MR2)) {
    341                     return DEQUEUE_FAIL;
    342                 }
    343                 if (!renderToSurface) {
    344                     ByteBuffer outputBuffer;
    345                     if (ApiLevelUtil.isBefore(Build.VERSION_CODES.LOLLIPOP)) {
    346                         outputBuffer = decoder.getOutputBuffers()[decoderStatus];
    347                     } else {
    348                         outputBuffer = decoder.getOutputBuffer(decoderStatus);
    349                     }
    350                     outputBytes = new byte[info.size];
    351                     outputBuffer.get(outputBytes);
    352                     outputBuffer.clear();
    353                 }
    354                 decoder.releaseOutputBuffer(decoderStatus, renderToSurface);
    355                 return DEQUEUE_SUCCESS;
    356             }
    357             return DEQUEUE_FAIL;
    358         }
    359 
    360         public void release() {
    361             decoderRelease();
    362             extractorRelease();
    363         }
    364 
    365         private boolean setExtractorDataSource(VideoFormat videoFormat) {
    366             checkNotNull(videoFormat);
    367             try {
    368                 final AssetFileDescriptor afd = videoFormat.getAssetFileDescriptor(context);
    369                 extractor.setDataSource(
    370                         afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
    371                 afd.close();
    372             } catch (IOException exception) {
    373                 Log.e(TAG, "IOException in setDataSource", exception);
    374                 return false;
    375             }
    376             return true;
    377         }
    378 
    379         /**
    380          * Creates a decoder based on conditions.
    381          *
    382          * <p>If codec name is provided, {@link MediaCodec#createByCodecName(String)} is used.
    383          * If codec name is not provided, {@link MediaCodecList#findDecoderForFormat(MediaFormat)}
    384          * is preferred on LOLLIPOP and up for finding out the codec name that
    385          * supports the media format.
    386          * For OS older than LOLLIPOP, {@link MediaCodec#createDecoderByType(String)} is used.
    387          */
    388         private boolean createDecoder(MediaFormat mediaFormat) {
    389             try {
    390                 if (codecName != null) {
    391                     decoder = MediaCodec.createByCodecName(codecName);
    392                 } else if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.LOLLIPOP)) {
    393                     if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
    394                         // On LOLLIPOP, format must not contain a frame rate.
    395                         mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null);
    396                     }
    397                     if (mediaCodecList == null) {
    398                         mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
    399                     }
    400                     decoder = MediaCodec.createByCodecName(
    401                             mediaCodecList.findDecoderForFormat(mediaFormat));
    402                 } else {
    403                     decoder = MediaCodec.createDecoderByType(
    404                             mediaFormat.getString(MediaFormat.KEY_MIME));
    405                 }
    406             } catch (Exception exception) {
    407                 Log.e(TAG, "Exception during decoder creation", exception);
    408                 decoderRelease();
    409                 return false;
    410             }
    411             return true;
    412         }
    413 
    414         private boolean configureDecoder(Surface surface, MediaFormat mediaFormat) {
    415             try {
    416                 decoder.configure(mediaFormat, surface, null, 0);
    417             } catch (Exception exception) {
    418                 Log.e(TAG, "Exception during decoder configuration", exception);
    419                 try {
    420                     decoder.reset();
    421                 } catch (Exception resetException) {
    422                     Log.e(TAG, "Exception during decoder reset", resetException);
    423                 }
    424                 decoderRelease();
    425                 return false;
    426             }
    427             return true;
    428         }
    429 
    430         private void setRenderToSurface(boolean render) {
    431             this.renderToSurface = render;
    432         }
    433 
    434         private boolean startDecoder() {
    435             try {
    436                 decoder.start();
    437             } catch (Exception exception) {
    438                 Log.e(TAG, "Exception during decoder start", exception);
    439                 decoder.reset();
    440                 decoderRelease();
    441                 return false;
    442             }
    443             return true;
    444         }
    445 
    446         private void decoderRelease() {
    447             if (decoder == null) {
    448                 return;
    449             }
    450             try {
    451                 decoder.stop();
    452             } catch (IllegalStateException exception) {
    453                 decoder.reset();
    454                 // IllegalStateException happens when decoder fail to start.
    455                 Log.e(TAG, "IllegalStateException during decoder stop", exception);
    456             } finally {
    457                 try {
    458                     decoder.release();
    459                 } catch (IllegalStateException exception) {
    460                     Log.e(TAG, "IllegalStateException during decoder release", exception);
    461                 }
    462                 decoder = null;
    463             }
    464         }
    465 
    466         private void extractorRelease() {
    467             if (extractor == null) {
    468                 return;
    469             }
    470             try {
    471                 extractor.release();
    472             } catch (IllegalStateException exception) {
    473                 Log.e(TAG, "IllegalStateException during extractor release", exception);
    474             }
    475         }
    476 
    477         private static void configureVideoFormat(MediaFormat mediaFormat, VideoFormat videoFormat) {
    478             checkNotNull(mediaFormat);
    479             checkNotNull(videoFormat);
    480             videoFormat.setMimeType(mediaFormat.getString(MediaFormat.KEY_MIME));
    481             videoFormat.setWidth(mediaFormat.getInteger(MediaFormat.KEY_WIDTH));
    482             videoFormat.setHeight(mediaFormat.getInteger(MediaFormat.KEY_HEIGHT));
    483             mediaFormat.setInteger(MediaFormat.KEY_WIDTH, videoFormat.getWidth());
    484             mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, videoFormat.getHeight());
    485             if (ApiLevelUtil.isBefore(Build.VERSION_CODES.KITKAT)) {
    486                 return;
    487             }
    488             // Set KEY_MAX_WIDTH and KEY_MAX_HEIGHT when isAbrEnabled() is set.
    489             if (videoFormat.isAbrEnabled()) {
    490                 try {
    491                     // Check for max resolution supported by the codec.
    492                     final MediaCodec decoder = MediaUtils.getDecoder(mediaFormat);
    493                     final VideoCapabilities videoCapabilities = MediaUtils.getVideoCapabilities(
    494                             decoder.getName(), videoFormat.getMimeType());
    495                     decoder.release();
    496                     final int maxWidth = videoCapabilities.getSupportedWidths().getUpper();
    497                     final int maxHeight =
    498                             videoCapabilities.getSupportedHeightsFor(maxWidth).getUpper();
    499                     if (maxWidth >= videoFormat.getWidth() && maxHeight >= videoFormat.getHeight()) {
    500                         mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, maxWidth);
    501                         mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, maxHeight);
    502                         return;
    503                     }
    504                 } catch (NullPointerException exception) { /* */ }
    505                 // Set max width/height to current size if can't get codec's max supported
    506                 // width/height or max is not greater than the current size.
    507                 mediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, videoFormat.getWidth());
    508                 mediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, videoFormat.getHeight());
    509             }
    510         }
    511 
    512         /**
    513          * The function returns the first track found based on the media type.
    514          */
    515         private int getFirstTrackIndexByType(String format) {
    516             for (int i = 0; i < extractor.getTrackCount(); i++) {
    517                 MediaFormat trackMediaFormat = extractor.getTrackFormat(i);
    518                 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(format + "/")) {
    519                     return i;
    520                 }
    521             }
    522             Log.e(TAG, "couldn't get a " + format + " track");
    523             return NO_TRACK_INDEX;
    524         }
    525 
    526         /**
    527          * Stores the result from SimplePlayer.
    528          */
    529         public static final class PlayerResult {
    530 
    531             public static final int UNSET = -1;
    532             private final boolean configureSuccess;
    533             private final boolean startSuccess;
    534             private final boolean decodeSuccess;
    535             private final long totalTime;
    536 
    537             public PlayerResult(
    538                     boolean configureSuccess, boolean startSuccess,
    539                     boolean decodeSuccess, long totalTime) {
    540                 this.configureSuccess = configureSuccess;
    541                 this.startSuccess = startSuccess;
    542                 this.decodeSuccess = decodeSuccess;
    543                 this.totalTime = totalTime;
    544             }
    545 
    546             public PlayerResult(PlayerResult playerResult) {
    547                 this(playerResult.configureSuccess, playerResult.startSuccess,
    548                         playerResult.decodeSuccess, playerResult.totalTime);
    549             }
    550 
    551             public PlayerResult() {
    552                 // Dummy PlayerResult.
    553                 this(false, false, false, UNSET);
    554             }
    555 
    556             public static PlayerResult failToStart() {
    557                 return new PlayerResult(true, false, false, UNSET);
    558             }
    559 
    560             public String getFailureMessage() {
    561                 if (!configureSuccess) {
    562                     return "Failed to configure decoder.";
    563                 } else if (!startSuccess) {
    564                     return "Failed to start decoder.";
    565                 } else if (!decodeSuccess) {
    566                     return "Failed to decode the expected number of frames.";
    567                 } else {
    568                     return "Failed to finish decoding.";
    569                 }
    570             }
    571 
    572             public boolean isConfigureSuccess() {
    573                 return configureSuccess;
    574             }
    575 
    576             public boolean isSuccess() {
    577                 return configureSuccess && startSuccess && decodeSuccess && getTotalTime() != UNSET;
    578             }
    579 
    580             public long getTotalTime() {
    581                 return totalTime;
    582             }
    583 
    584         }
    585 
    586     }
    587 
    588     /* Utility class for collecting common test case functionality. */
    589     class TestHelper {
    590 
    591         private final String TAG =  TestHelper.class.getSimpleName();
    592 
    593         private final Context context;
    594         private final Handler handler;
    595         private final Activity activity;
    596 
    597         public TestHelper(Context context, Activity activity) {
    598             this.context = checkNotNull(context);
    599             this.handler = new Handler(Looper.getMainLooper());
    600             this.activity = activity;
    601         }
    602 
    603         public Bitmap generateBitmapFromImageResourceId(int resourceId) {
    604             return BitmapFactory.decodeStream(context.getResources().openRawResource(resourceId));
    605         }
    606 
    607         public Context getContext() {
    608             return context;
    609         }
    610 
    611         public void rotateOrientation() {
    612             handler.post(new Runnable() {
    613                 @Override
    614                 public void run() {
    615                     final int orientation = context.getResources().getConfiguration().orientation;
    616                     if (orientation == Configuration.ORIENTATION_PORTRAIT) {
    617                         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    618                     } else {
    619                         activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    620                     }
    621                 }
    622             });
    623         }
    624 
    625         public void unsetOrientation() {
    626             handler.post(new Runnable() {
    627                 @Override
    628                 public void run() {
    629                     activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
    630                 }
    631             });
    632         }
    633 
    634         public void generateView(View view) {
    635             RelativeLayout relativeLayout =
    636                     (RelativeLayout) activity.findViewById(R.id.attach_view);
    637             ViewGenerator viewGenerator = new ViewGenerator(relativeLayout, view);
    638             handler.post(viewGenerator);
    639         }
    640 
    641         public void cleanUpView(View view) {
    642             ViewCleaner viewCleaner = new ViewCleaner(view);
    643             handler.post(viewCleaner);
    644         }
    645 
    646         public Bitmap generateBitmapFromVideoViewSnapshot(VideoViewSnapshot snapshot) {
    647             handler.post(snapshot);
    648             synchronized (snapshot.getSyncObject()) {
    649                 try {
    650                     snapshot.getSyncObject().wait(snapshot.SNAPSHOT_TIMEOUT_MS + 100);
    651                 } catch (InterruptedException e) {
    652                     e.printStackTrace();
    653                     Log.e(TAG, "Unable to finish generateBitmapFromVideoViewSnapshot().");
    654                     return null;
    655                 }
    656             }
    657             if (!snapshot.isBitmapReady()) {
    658                 Log.e(TAG, "Time out in generateBitmapFromVideoViewSnapshot().");
    659                 return null;
    660             }
    661             return snapshot.getBitmap();
    662         }
    663 
    664         private class ViewGenerator implements Runnable {
    665 
    666             private final View view;
    667             private final RelativeLayout relativeLayout;
    668 
    669             public ViewGenerator(RelativeLayout relativeLayout, View view) {
    670                 this.view = checkNotNull(view);
    671                 this.relativeLayout = checkNotNull(relativeLayout);
    672             }
    673 
    674             @Override
    675             public void run() {
    676                 if (view.getParent() != null) {
    677                     ((ViewGroup) view.getParent()).removeView(view);
    678                 }
    679                 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
    680                         VideoViewFactory.VIEW_WIDTH, VideoViewFactory.VIEW_HEIGHT);
    681                 view.setLayoutParams(params);
    682                 relativeLayout.addView(view);
    683             }
    684 
    685         }
    686 
    687         private class ViewCleaner implements Runnable {
    688 
    689             private final View view;
    690 
    691             public ViewCleaner(View view) {
    692                 this.view = checkNotNull(view);
    693             }
    694 
    695             @Override
    696             public void run() {
    697                 if (view.getParent() != null) {
    698                     ((ViewGroup) view.getParent()).removeView(view);
    699                 }
    700             }
    701 
    702         }
    703 
    704     }
    705 
    706 }
    707 
    708 /* Factory for manipulating a {@link View}. */
    709 abstract class VideoViewFactory {
    710 
    711     public static final long VIEW_WAITTIME_MS = TimeUnit.SECONDS.toMillis(1);
    712     public static final long DEFAULT_VIEW_AVAILABLE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(3);
    713     public static final int VIEW_WIDTH = 480;
    714     public static final int VIEW_HEIGHT = 360;
    715 
    716     public VideoViewFactory() {}
    717 
    718     public abstract void release();
    719 
    720     public abstract String getName();
    721 
    722     public abstract View createView(Context context);
    723 
    724     public void waitForViewIsAvailable() throws Exception {
    725         waitForViewIsAvailable(DEFAULT_VIEW_AVAILABLE_TIMEOUT_MS);
    726     };
    727 
    728     public abstract void waitForViewIsAvailable(long timeOutMs) throws Exception;
    729 
    730     public abstract Surface getSurface();
    731 
    732     public abstract VideoViewSnapshot getVideoViewSnapshot();
    733 
    734     public boolean hasLooper() {
    735         return Looper.myLooper() != null;
    736     }
    737 
    738 }
    739 
    740 /* Factory for building a {@link TextureView}. */
    741 @TargetApi(16)
    742 class TextureViewFactory extends VideoViewFactory implements TextureView.SurfaceTextureListener {
    743 
    744     private static final String TAG = TextureViewFactory.class.getSimpleName();
    745     private static final String NAME = "TextureView";
    746 
    747     private final Object syncToken = new Object();
    748     private TextureView textureView;
    749 
    750     public TextureViewFactory() {}
    751 
    752     @Override
    753     public TextureView createView(Context context) {
    754         Log.i(TAG, "Creating a " + NAME);
    755         textureView = DecodeAccuracyTestBase.checkNotNull(new TextureView(context));
    756         textureView.setSurfaceTextureListener(this);
    757         return textureView;
    758     }
    759 
    760     @Override
    761     public void release() {
    762         textureView = null;
    763     }
    764 
    765     @Override
    766     public String getName() {
    767         return NAME;
    768     }
    769 
    770     @Override
    771     public Surface getSurface() {
    772         return new Surface(textureView.getSurfaceTexture());
    773     }
    774 
    775     @Override
    776     public TextureViewSnapshot getVideoViewSnapshot() {
    777         return new TextureViewSnapshot(textureView);
    778     }
    779 
    780     @Override
    781     public void waitForViewIsAvailable(long timeOutMs) throws Exception {
    782         final long start = SystemClock.elapsedRealtime();
    783         while (SystemClock.elapsedRealtime() - start < timeOutMs && !textureView.isAvailable()) {
    784             synchronized (syncToken) {
    785                 try {
    786                     syncToken.wait(VIEW_WAITTIME_MS);
    787                 } catch (InterruptedException e) {
    788                     Log.e(TAG, "Exception occurred when attaching a TextureView to a window.", e);
    789                     throw new InterruptedException(e.getMessage());
    790                 }
    791             }
    792         }
    793         if (!textureView.isAvailable()) {
    794             throw new InterruptedException("Taking too long to attach a TextureView to a window.");
    795         }
    796         Log.i(TAG, NAME + " is available.");
    797     }
    798 
    799     @Override
    800     public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
    801         synchronized (syncToken) {
    802             syncToken.notify();
    803         }
    804     }
    805 
    806     @Override
    807     public void onSurfaceTextureSizeChanged(
    808             SurfaceTexture surfaceTexture, int width, int height) {}
    809 
    810     @Override
    811     public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
    812         return false;
    813     }
    814 
    815     @Override
    816     public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {}
    817 
    818 }
    819 
    820 /**
    821  * Factory for building a {@link SurfaceView}
    822  */
    823 @TargetApi(24)
    824 class SurfaceViewFactory extends VideoViewFactory implements SurfaceHolder.Callback {
    825 
    826     private static final String TAG = SurfaceViewFactory.class.getSimpleName();
    827     private static final String NAME = "SurfaceView";
    828     private final Object syncToken = new Object();
    829 
    830     private SurfaceView surfaceView;
    831     private SurfaceHolder surfaceHolder;
    832 
    833     public SurfaceViewFactory() {}
    834 
    835     @Override
    836     public void release() {
    837         surfaceView = null;
    838         surfaceHolder = null;
    839     }
    840 
    841     @Override
    842     public String getName() {
    843         return NAME;
    844     }
    845 
    846     @Override
    847     public View createView(Context context) {
    848         Log.i(TAG, "Creating a " + NAME);
    849         if (!super.hasLooper()) {
    850             Looper.prepare();
    851         }
    852         surfaceView = new SurfaceView(context);
    853         surfaceHolder = surfaceView.getHolder();
    854         surfaceHolder.addCallback(this);
    855         return surfaceView;
    856     }
    857 
    858     @Override
    859     public void waitForViewIsAvailable(long timeOutMs) throws Exception {
    860         final long start = SystemClock.elapsedRealtime();
    861         while (SystemClock.elapsedRealtime() - start < timeOutMs && !getSurface().isValid()) {
    862             synchronized (syncToken) {
    863                 try {
    864                     syncToken.wait(VIEW_WAITTIME_MS);
    865                 } catch (InterruptedException e) {
    866                     Log.e(TAG, "Exception occurred when attaching a SurfaceView to a window.", e);
    867                     throw new InterruptedException(e.getMessage());
    868                 }
    869             }
    870         }
    871         if (!getSurface().isValid()) {
    872             throw new InterruptedException("Taking too long to attach a SurfaceView to a window.");
    873         }
    874         Log.i(TAG, NAME + " is available.");
    875     }
    876 
    877     @Override
    878     public Surface getSurface() {
    879         return surfaceHolder == null ? null : surfaceHolder.getSurface();
    880     }
    881 
    882     @Override
    883     public VideoViewSnapshot getVideoViewSnapshot() {
    884         return new SurfaceViewSnapshot(surfaceView, VIEW_WIDTH, VIEW_HEIGHT);
    885     }
    886 
    887     @Override
    888     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
    889 
    890     @Override
    891     public void surfaceCreated(SurfaceHolder holder) {
    892         synchronized (syncToken) {
    893             syncToken.notify();
    894         }
    895     }
    896 
    897     @Override
    898     public void surfaceDestroyed(SurfaceHolder holder) {}
    899 
    900 }
    901 
    902 /**
    903  * Factory for building EGL and GLES that could render to GLSurfaceView.
    904  * {@link GLSurfaceView} {@link EGL10} {@link GLES20}.
    905  */
    906 @TargetApi(16)
    907 class GLSurfaceViewFactory extends VideoViewFactory {
    908 
    909     private static final String TAG = GLSurfaceViewFactory.class.getSimpleName();
    910     private static final String NAME = "GLSurfaceView";
    911 
    912     private final Object surfaceSyncToken = new Object();
    913 
    914     private GLSurfaceViewThread glSurfaceViewThread;
    915     private boolean byteBufferIsReady = false;
    916 
    917     public GLSurfaceViewFactory() {}
    918 
    919     @Override
    920     public void release() {
    921         glSurfaceViewThread.release();
    922         glSurfaceViewThread = null;
    923     }
    924 
    925     @Override
    926     public String getName() {
    927         return NAME;
    928     }
    929 
    930     @Override
    931     public View createView(Context context) {
    932         Log.i(TAG, "Creating a " + NAME);
    933         // Do all GL rendering in the GL thread.
    934         glSurfaceViewThread = new GLSurfaceViewThread();
    935         glSurfaceViewThread.start();
    936         // No necessary view to display, return null.
    937         return null;
    938     }
    939 
    940     @Override
    941     public void waitForViewIsAvailable(long timeOutMs) throws Exception {
    942         final long start = SystemClock.elapsedRealtime();
    943         while (SystemClock.elapsedRealtime() - start < timeOutMs
    944                 && glSurfaceViewThread.getSurface() == null) {
    945             synchronized (surfaceSyncToken) {
    946                 try {
    947                     surfaceSyncToken.wait(VIEW_WAITTIME_MS);
    948                 } catch (InterruptedException e) {
    949                     Log.e(TAG, "Exception occurred when waiting for the surface from"
    950                             + " GLSurfaceView to become available.", e);
    951                     throw new InterruptedException(e.getMessage());
    952                 }
    953             }
    954         }
    955         if (glSurfaceViewThread.getSurface() == null) {
    956             throw new InterruptedException("Taking too long for the surface from"
    957                     + " GLSurfaceView to become available.");
    958         }
    959         Log.i(TAG, NAME + " is available.");
    960     }
    961 
    962     @Override
    963     public Surface getSurface() {
    964         return glSurfaceViewThread.getSurface();
    965     }
    966 
    967     @Override
    968     public VideoViewSnapshot getVideoViewSnapshot() {
    969         return new GLSurfaceViewSnapshot(this, VIEW_WIDTH, VIEW_HEIGHT);
    970     }
    971 
    972     public boolean byteBufferIsReady() {
    973         return byteBufferIsReady;
    974     }
    975 
    976     public ByteBuffer getByteBuffer() {
    977         return glSurfaceViewThread.getByteBuffer();
    978     }
    979 
    980     /* Does all GL operations. */
    981     private class GLSurfaceViewThread extends Thread
    982             implements SurfaceTexture.OnFrameAvailableListener {
    983 
    984         private static final int FLOAT_SIZE_BYTES = 4;
    985         private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
    986         private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
    987         private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
    988         private FloatBuffer triangleVertices;
    989         private float[] textureTransform = new float[16];
    990 
    991         private float[] triangleVerticesData = {
    992             // X, Y, Z, U, V
    993             -1f, -1f,  0f,  0f,  1f,
    994              1f, -1f,  0f,  1f,  1f,
    995             -1f,  1f,  0f,  0f,  0f,
    996              1f,  1f,  0f,  1f,  0f,
    997         };
    998         // Make the top-left corner corresponds to texture coordinate
    999         // (0, 0). This complies with the transformation matrix obtained from
   1000         // SurfaceTexture.getTransformMatrix.
   1001 
   1002         private static final String VERTEX_SHADER =
   1003                 "attribute vec4 aPosition;\n"
   1004                 + "attribute vec4 aTextureCoord;\n"
   1005                 + "uniform mat4 uTextureTransform;\n"
   1006                 + "varying vec2 vTextureCoord;\n"
   1007                 + "void main() {\n"
   1008                 + "    gl_Position = aPosition;\n"
   1009                 + "    vTextureCoord = (uTextureTransform * aTextureCoord).xy;\n"
   1010                 + "}\n";
   1011 
   1012         private static final String FRAGMENT_SHADER =
   1013                 "#extension GL_OES_EGL_image_external : require\n"
   1014                 + "precision mediump float;\n"      // highp here doesn't seem to matter
   1015                 + "varying vec2 vTextureCoord;\n"
   1016                 + "uniform samplerExternalOES sTexture;\n"
   1017                 + "void main() {\n"
   1018                 + "    gl_FragColor = texture2D(sTexture, vTextureCoord);\n"
   1019                 + "}\n";
   1020 
   1021         private int glProgram;
   1022         private int textureID = -1;
   1023         private int aPositionHandle;
   1024         private int aTextureHandle;
   1025         private int uTextureTransformHandle;
   1026         private EGLDisplay eglDisplay = null;
   1027         private EGLContext eglContext = null;
   1028         private EGLSurface eglSurface = null;
   1029         private EGL10 egl10;
   1030         private Surface surface = null;
   1031         private SurfaceTexture surfaceTexture;
   1032         private ByteBuffer byteBuffer;
   1033         private Looper looper;
   1034 
   1035         public GLSurfaceViewThread() {}
   1036 
   1037         @Override
   1038         public void run() {
   1039             Looper.prepare();
   1040             looper = Looper.myLooper();
   1041             triangleVertices = ByteBuffer
   1042                     .allocateDirect(triangleVerticesData.length * FLOAT_SIZE_BYTES)
   1043                     .order(ByteOrder.nativeOrder()).asFloatBuffer();
   1044             triangleVertices.put(triangleVerticesData).position(0);
   1045 
   1046             eglSetup();
   1047             makeCurrent();
   1048             eglSurfaceCreated();
   1049 
   1050             surfaceTexture = new SurfaceTexture(getTextureId());
   1051             surfaceTexture.setOnFrameAvailableListener(this);
   1052             surface = new Surface(surfaceTexture);
   1053             synchronized (surfaceSyncToken) {
   1054                 surfaceSyncToken.notify();
   1055             }
   1056             // Store pixels from surface
   1057             byteBuffer = ByteBuffer.allocateDirect(VIEW_WIDTH * VIEW_HEIGHT * 4);
   1058             byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
   1059             Looper.loop();
   1060         }
   1061 
   1062         @Override
   1063         public void onFrameAvailable(SurfaceTexture st) {
   1064             checkGlError("before updateTexImage");
   1065             surfaceTexture.updateTexImage();
   1066             st.getTransformMatrix(textureTransform);
   1067             drawFrame();
   1068             saveFrame();
   1069         }
   1070 
   1071         /* Prepares EGL to use GLES 2.0 context and a surface that supports pbuffer. */
   1072         public void eglSetup() {
   1073             egl10 = (EGL10) EGLContext.getEGL();
   1074             eglDisplay = egl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
   1075             if (eglDisplay == EGL10.EGL_NO_DISPLAY) {
   1076                 throw new RuntimeException("unable to get egl10 display");
   1077             }
   1078             int[] version = new int[2];
   1079             if (!egl10.eglInitialize(eglDisplay, version)) {
   1080                 eglDisplay = null;
   1081                 throw new RuntimeException("unable to initialize egl10");
   1082             }
   1083             // Configure EGL for pbuffer and OpenGL ES 2.0, 24-bit RGB.
   1084             int[] configAttribs = {
   1085                 EGL10.EGL_RED_SIZE, 8,
   1086                 EGL10.EGL_GREEN_SIZE, 8,
   1087                 EGL10.EGL_BLUE_SIZE, 8,
   1088                 EGL10.EGL_ALPHA_SIZE, 8,
   1089                 EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
   1090                 EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
   1091                 EGL10.EGL_NONE
   1092             };
   1093             EGLConfig[] configs = new EGLConfig[1];
   1094             int[] numConfigs = new int[1];
   1095             if (!egl10.eglChooseConfig(
   1096                     eglDisplay, configAttribs, configs, configs.length, numConfigs)) {
   1097                 throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
   1098             }
   1099             // Configure EGL context for OpenGL ES 2.0.
   1100             int[] contextAttribs = {
   1101                 EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
   1102                 EGL10.EGL_NONE
   1103             };
   1104             eglContext = egl10.eglCreateContext(
   1105                     eglDisplay, configs[0], EGL10.EGL_NO_CONTEXT, contextAttribs);
   1106             checkEglError("eglCreateContext");
   1107             if (eglContext == null) {
   1108                 throw new RuntimeException("null context");
   1109             }
   1110             // Create a pbuffer surface.
   1111             int[] surfaceAttribs = {
   1112                 EGL10.EGL_WIDTH, VIEW_WIDTH,
   1113                 EGL10.EGL_HEIGHT, VIEW_HEIGHT,
   1114                 EGL10.EGL_NONE
   1115             };
   1116             eglSurface = egl10.eglCreatePbufferSurface(eglDisplay, configs[0], surfaceAttribs);
   1117             checkEglError("eglCreatePbufferSurface");
   1118             if (eglSurface == null) {
   1119                 throw new RuntimeException("surface was null");
   1120             }
   1121         }
   1122 
   1123         public void release() {
   1124             looper.quit();
   1125             if (eglDisplay != EGL10.EGL_NO_DISPLAY) {
   1126                 egl10.eglMakeCurrent(eglDisplay,
   1127                         EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
   1128                 egl10.eglDestroySurface(eglDisplay, eglSurface);
   1129                 egl10.eglDestroyContext(eglDisplay, eglContext);
   1130                 egl10.eglTerminate(eglDisplay);
   1131             }
   1132             eglDisplay = EGL10.EGL_NO_DISPLAY;
   1133             eglContext = EGL10.EGL_NO_CONTEXT;
   1134             eglSurface = EGL10.EGL_NO_SURFACE;
   1135             surface.release();
   1136             surfaceTexture.release();
   1137             byteBufferIsReady = false;
   1138             byteBuffer =  null;
   1139         }
   1140 
   1141         /* Makes our EGL context and surface current. */
   1142         public void makeCurrent() {
   1143             if (!egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
   1144                 throw new RuntimeException("eglMakeCurrent failed");
   1145             }
   1146             checkEglError("eglMakeCurrent");
   1147         }
   1148 
   1149         /* Call this after the EGL Surface is created and made current. */
   1150         public void eglSurfaceCreated() {
   1151             glProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
   1152             if (glProgram == 0) {
   1153                 throw new RuntimeException("failed creating program");
   1154             }
   1155             aPositionHandle = GLES20.glGetAttribLocation(glProgram, "aPosition");
   1156             checkLocation(aPositionHandle, "aPosition");
   1157             aTextureHandle = GLES20.glGetAttribLocation(glProgram, "aTextureCoord");
   1158             checkLocation(aTextureHandle, "aTextureCoord");
   1159             uTextureTransformHandle = GLES20.glGetUniformLocation(glProgram, "uTextureTransform");
   1160             checkLocation(uTextureTransformHandle, "uTextureTransform");
   1161 
   1162             int[] textures = new int[1];
   1163             GLES20.glGenTextures(1, textures, 0);
   1164             checkGlError("glGenTextures");
   1165             textureID = textures[0];
   1166             GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureID);
   1167             checkGlError("glBindTexture");
   1168 
   1169             GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
   1170                     GLES20.GL_LINEAR);
   1171             GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
   1172                     GLES20.GL_LINEAR);
   1173             GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
   1174                     GLES20.GL_CLAMP_TO_EDGE);
   1175             GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
   1176                     GLES20.GL_CLAMP_TO_EDGE);
   1177             checkGlError("glTexParameter");
   1178         }
   1179 
   1180         public void drawFrame() {
   1181             GLES20.glUseProgram(glProgram);
   1182             checkGlError("glUseProgram");
   1183             GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
   1184             checkGlError("glActiveTexture");
   1185             GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureID);
   1186             checkGlError("glBindTexture");
   1187 
   1188             triangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
   1189             GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false,
   1190                     TRIANGLE_VERTICES_DATA_STRIDE_BYTES, triangleVertices);
   1191             checkGlError("glVertexAttribPointer aPositionHandle");
   1192             GLES20.glEnableVertexAttribArray(aPositionHandle);
   1193             checkGlError("glEnableVertexAttribArray aPositionHandle");
   1194 
   1195             triangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);
   1196             GLES20.glVertexAttribPointer(aTextureHandle, 2, GLES20.GL_FLOAT, false,
   1197                     TRIANGLE_VERTICES_DATA_STRIDE_BYTES, triangleVertices);
   1198             checkGlError("glVertexAttribPointer aTextureHandle");
   1199             GLES20.glEnableVertexAttribArray(aTextureHandle);
   1200             checkGlError("glEnableVertexAttribArray aTextureHandle");
   1201 
   1202             GLES20.glUniformMatrix4fv(uTextureTransformHandle, 1, false, textureTransform, 0);
   1203             checkGlError("glUniformMatrix uTextureTransformHandle");
   1204 
   1205             GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
   1206             checkGlError("glDrawArrays");
   1207             GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
   1208         }
   1209 
   1210         /* Reads the pixels to a ByteBuffer. */
   1211         public void saveFrame() {
   1212             byteBufferIsReady = false;
   1213             byteBuffer.clear();
   1214             GLES20.glReadPixels(0, 0, VIEW_WIDTH, VIEW_HEIGHT, GLES20.GL_RGBA,
   1215                     GLES20.GL_UNSIGNED_BYTE, byteBuffer);
   1216             byteBufferIsReady = true;
   1217         }
   1218 
   1219         public int getTextureId() {
   1220             return textureID;
   1221         }
   1222 
   1223         public Surface getSurface() {
   1224             return surface;
   1225         }
   1226 
   1227         public ByteBuffer getByteBuffer() {
   1228             return byteBuffer;
   1229         }
   1230 
   1231         private int loadShader(int shaderType, String source) {
   1232             int shader = GLES20.glCreateShader(shaderType);
   1233             checkGlError("glCreateShader type=" + shaderType);
   1234             GLES20.glShaderSource(shader, source);
   1235             GLES20.glCompileShader(shader);
   1236             int[] compiled = new int[1];
   1237             GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
   1238 
   1239             if (compiled[0] == 0) {
   1240                 Log.e(TAG, "Could not compile shader " + shaderType + ":");
   1241                 Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader));
   1242                 GLES20.glDeleteShader(shader);
   1243                 shader = 0;
   1244             }
   1245             return shader;
   1246         }
   1247 
   1248         private int createProgram(String vertexSource, String fragmentSource) {
   1249             int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
   1250             if (vertexShader == 0) {
   1251                 return 0;
   1252             }
   1253             int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
   1254             if (pixelShader == 0) {
   1255                 return 0;
   1256             }
   1257             int program = GLES20.glCreateProgram();
   1258             if (program == 0) {
   1259                 Log.e(TAG, "Could not create program");
   1260             }
   1261             GLES20.glAttachShader(program, vertexShader);
   1262             checkGlError("glAttachShader");
   1263             GLES20.glAttachShader(program, pixelShader);
   1264             checkGlError("glAttachShader");
   1265             GLES20.glLinkProgram(program);
   1266             int[] linkStatus = new int[1];
   1267             GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
   1268 
   1269             if (linkStatus[0] != GLES20.GL_TRUE) {
   1270                 Log.e(TAG, "Could not link program: ");
   1271                 Log.e(TAG, GLES20.glGetProgramInfoLog(program));
   1272                 GLES20.glDeleteProgram(program);
   1273                 program = 0;
   1274             }
   1275             return program;
   1276         }
   1277 
   1278         private void checkEglError(String msg) {
   1279             int error;
   1280             if ((error = egl10.eglGetError()) != EGL10.EGL_SUCCESS) {
   1281                 throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
   1282             }
   1283         }
   1284 
   1285         public void checkGlError(String op) {
   1286             int error;
   1287             if ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
   1288                 Log.e(TAG, op + ": glError " + error);
   1289                 throw new RuntimeException(op + ": glError " + error);
   1290             }
   1291         }
   1292 
   1293         public void checkLocation(int location, String label) {
   1294             if (location < 0) {
   1295                 throw new RuntimeException("Unable to locate '" + label + "' in program");
   1296             }
   1297         }
   1298     }
   1299 
   1300 }
   1301 
   1302 /* Definition of a VideoViewSnapshot and a runnable to get a bitmap from a view. */
   1303 abstract class VideoViewSnapshot implements Runnable {
   1304 
   1305     public static final long SNAPSHOT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30);
   1306     public static final long SLEEP_TIME_MS = 30;
   1307     public static final Object SYNC_TOKEN = new Object();
   1308 
   1309     public abstract Bitmap getBitmap();
   1310 
   1311     public abstract boolean isBitmapReady();
   1312 
   1313     public abstract Object getSyncObject();
   1314 
   1315 }
   1316 
   1317 /* Runnable to get a bitmap from a texture view on the UI thread via a handler.
   1318  * This class is to be used together with
   1319  * {@link TestHelper#generateBitmapFromVideoViewSnapshot(VideoViewSnapshot)}
   1320  */
   1321 class TextureViewSnapshot extends VideoViewSnapshot {
   1322 
   1323     private final TextureView tv;
   1324     private Bitmap bitmap = null;
   1325 
   1326     public TextureViewSnapshot(TextureView tv) {
   1327         this.tv = DecodeAccuracyTestBase.checkNotNull(tv);
   1328     }
   1329 
   1330     @Override
   1331     public void run() {
   1332         bitmap = null;
   1333         bitmap = tv.getBitmap();
   1334         synchronized (SYNC_TOKEN) {
   1335             SYNC_TOKEN.notify();
   1336         }
   1337     }
   1338 
   1339     @Override
   1340     public Bitmap getBitmap() {
   1341         return bitmap;
   1342     }
   1343 
   1344     @Override
   1345     public boolean isBitmapReady() {
   1346         return bitmap != null;
   1347     }
   1348 
   1349     @Override
   1350     public Object getSyncObject() {
   1351         return SYNC_TOKEN;
   1352     }
   1353 
   1354 }
   1355 
   1356 /**
   1357  * Method to get bitmap of a {@link SurfaceView}.
   1358  * Note that PixelCopy does not have to be called in a runnable.
   1359  * This class is to be used together with
   1360  * {@link TestHelper#generateBitmapFromVideoViewSnapshot(VideoViewSnapshot)}
   1361  */
   1362 class SurfaceViewSnapshot extends VideoViewSnapshot  {
   1363 
   1364     private static final String TAG = SurfaceViewSnapshot.class.getSimpleName();
   1365     private static final int PIXELCOPY_TIMEOUT_MS = 1000;
   1366     private static final int INITIAL_STATE = -1;
   1367 
   1368     private final SurfaceView surfaceView;
   1369     private final int width;
   1370     private final int height;
   1371 
   1372     private Bitmap bitmap;
   1373     private int copyResult;
   1374 
   1375     public SurfaceViewSnapshot(SurfaceView surfaceView, int width, int height) {
   1376         this.surfaceView = surfaceView;
   1377         this.width = width;
   1378         this.height = height;
   1379         this.copyResult = INITIAL_STATE;
   1380         this.bitmap = null;
   1381     }
   1382 
   1383     @Override
   1384     public void run() {
   1385         final long start = SystemClock.elapsedRealtime();
   1386         copyResult = INITIAL_STATE;
   1387         final SynchronousPixelCopy copyHelper = new SynchronousPixelCopy();
   1388         bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
   1389         try {
   1390             // Wait for PixelCopy to finish.
   1391             while ((copyResult = copyHelper.request(surfaceView, bitmap)) != PixelCopy.SUCCESS
   1392                     && (SystemClock.elapsedRealtime() - start) < SNAPSHOT_TIMEOUT_MS) {
   1393                 Thread.sleep(SLEEP_TIME_MS);
   1394             }
   1395         } catch (InterruptedException e) {
   1396             Log.e(TAG, "Pixel Copy is stopped/interrupted before it finishes.", e);
   1397             bitmap = null;
   1398         } finally {
   1399             copyHelper.release();
   1400             synchronized (SYNC_TOKEN) {
   1401                 SYNC_TOKEN.notify();
   1402             }
   1403         }
   1404     }
   1405 
   1406     @Override
   1407     public Bitmap getBitmap() {
   1408         return bitmap;
   1409     }
   1410 
   1411     @Override
   1412     public boolean isBitmapReady() {
   1413         return bitmap != null && copyResult == PixelCopy.SUCCESS;
   1414     }
   1415 
   1416     @Override
   1417     public Object getSyncObject() {
   1418         return SYNC_TOKEN;
   1419     }
   1420 
   1421     private static class SynchronousPixelCopy implements OnPixelCopyFinishedListener {
   1422 
   1423         private final Handler handler;
   1424         private final HandlerThread thread;
   1425 
   1426         private int status = INITIAL_STATE;
   1427 
   1428         public SynchronousPixelCopy() {
   1429             this.thread = new HandlerThread("PixelCopyHelper");
   1430             thread.start();
   1431             this.handler = new Handler(thread.getLooper());
   1432         }
   1433 
   1434         public void release() {
   1435             if (thread.isAlive()) {
   1436                 thread.quit();
   1437             }
   1438         }
   1439 
   1440         public int request(SurfaceView source, Bitmap dest) {
   1441             synchronized (this) {
   1442                 try {
   1443                     PixelCopy.request(source, dest, this, handler);
   1444                     return getResultLocked();
   1445                 } catch (Exception e) {
   1446                     Log.e(TAG, "Exception occurred when copying a SurfaceView.", e);
   1447                     return -1;
   1448                 }
   1449             }
   1450         }
   1451 
   1452         private int getResultLocked() {
   1453             try {
   1454                 this.wait(PIXELCOPY_TIMEOUT_MS);
   1455             } catch (InterruptedException e) { /* PixelCopy request didn't complete within 1s */ }
   1456             return status;
   1457         }
   1458 
   1459         @Override
   1460         public void onPixelCopyFinished(int copyResult) {
   1461             synchronized (this) {
   1462                 status = copyResult;
   1463                 this.notify();
   1464             }
   1465         }
   1466 
   1467     }
   1468 
   1469 }
   1470 
   1471 /**
   1472  * Runnable to get a bitmap from a GLSurfaceView on the UI thread via a handler.
   1473  * Note, because of how the bitmap is captured in GLSurfaceView,
   1474  * this method does not have to be a runnable.
   1475   * This class is to be used together with
   1476  * {@link TestHelper#generateBitmapFromVideoViewSnapshot(VideoViewSnapshot)}
   1477  */
   1478 class GLSurfaceViewSnapshot extends VideoViewSnapshot {
   1479 
   1480     private static final String TAG = GLSurfaceViewSnapshot.class.getSimpleName();
   1481 
   1482     private final GLSurfaceViewFactory glSurfaceViewFactory;
   1483     private final int width;
   1484     private final int height;
   1485 
   1486     private Bitmap bitmap = null;
   1487     private boolean bitmapIsReady = false;
   1488 
   1489     public GLSurfaceViewSnapshot(GLSurfaceViewFactory glSurfaceViewFactory, int width, int height) {
   1490         this.glSurfaceViewFactory = DecodeAccuracyTestBase.checkNotNull(glSurfaceViewFactory);
   1491         this.width = width;
   1492         this.height = height;
   1493     }
   1494 
   1495     @Override
   1496     public void run() {
   1497         bitmapIsReady = false;
   1498         bitmap = null;
   1499         try {
   1500             waitForByteBuffer();
   1501         } catch (InterruptedException exception) {
   1502             Log.e(TAG, exception.getMessage());
   1503             bitmap = null;
   1504             notifyObject();
   1505             return;
   1506         }
   1507         try {
   1508             final ByteBuffer byteBuffer = glSurfaceViewFactory.getByteBuffer();
   1509             bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
   1510             byteBuffer.rewind();
   1511             bitmap.copyPixelsFromBuffer(byteBuffer);
   1512             bitmapIsReady = true;
   1513             byteBuffer.clear();
   1514         } catch (NullPointerException exception) {
   1515             Log.e(TAG, "glSurfaceViewFactory or byteBuffer may have been released", exception);
   1516             bitmap = null;
   1517         } finally {
   1518             notifyObject();
   1519         }
   1520     }
   1521 
   1522     @Override
   1523     public Bitmap getBitmap() {
   1524         return bitmap;
   1525     }
   1526 
   1527     @Override
   1528     public boolean isBitmapReady() {
   1529         return bitmapIsReady;
   1530     }
   1531 
   1532     @Override
   1533     public Object getSyncObject() {
   1534         return SYNC_TOKEN;
   1535     }
   1536 
   1537     private void notifyObject() {
   1538         synchronized (SYNC_TOKEN) {
   1539             SYNC_TOKEN.notify();
   1540         }
   1541     }
   1542 
   1543     private void waitForByteBuffer() throws InterruptedException {
   1544         // Wait for byte buffer to be ready.
   1545         final long start = SystemClock.elapsedRealtime();
   1546         while (SystemClock.elapsedRealtime() - start < SNAPSHOT_TIMEOUT_MS) {
   1547             if (glSurfaceViewFactory.byteBufferIsReady()) {
   1548                 return;
   1549             }
   1550             Thread.sleep(SLEEP_TIME_MS);
   1551         }
   1552         throw new InterruptedException("Taking too long to read pixels into a ByteBuffer.");
   1553     }
   1554 
   1555 }
   1556 
   1557 /* Stores information of a video file. */
   1558 class VideoFormat {
   1559 
   1560     public static final String STRING_UNSET = "UNSET";
   1561     public static final int INT_UNSET = -1;
   1562 
   1563     private final String filename;
   1564 
   1565     private String mimeType = STRING_UNSET;
   1566     private int width = INT_UNSET;
   1567     private int height = INT_UNSET;
   1568     private int maxWidth = INT_UNSET;
   1569     private int maxHeight = INT_UNSET;
   1570     private FilenameParser filenameParser;
   1571 
   1572     public VideoFormat(String filename) {
   1573         this.filename = filename;
   1574     }
   1575 
   1576     public VideoFormat(VideoFormat videoFormat) {
   1577         this(videoFormat.filename);
   1578     }
   1579 
   1580     private FilenameParser getParsedName() {
   1581         if (filenameParser == null) {
   1582             filenameParser = new FilenameParser(filename);
   1583         }
   1584         return filenameParser;
   1585     }
   1586 
   1587     public String getMediaFormat() {
   1588         return "video";
   1589     }
   1590 
   1591     public void setMimeType(String mimeType) {
   1592         this.mimeType = mimeType;
   1593     }
   1594 
   1595     public String getMimeType() {
   1596         if (mimeType.equals(STRING_UNSET)) {
   1597             return getParsedName().getMimeType();
   1598         }
   1599         return mimeType;
   1600     }
   1601 
   1602     public void setWidth(int width) {
   1603         this.width = width;
   1604     }
   1605 
   1606     public void setMaxWidth(int maxWidth) {
   1607         this.maxWidth = maxWidth;
   1608     }
   1609 
   1610     public int getWidth() {
   1611         if (width == INT_UNSET) {
   1612             return getParsedName().getWidth();
   1613         }
   1614         return width;
   1615     }
   1616 
   1617     public int getMaxWidth() {
   1618         return maxWidth;
   1619     }
   1620 
   1621     public int getOriginalWidth() {
   1622         return getParsedName().getWidth();
   1623     }
   1624 
   1625     public void setHeight(int height) {
   1626         this.height = height;
   1627     }
   1628 
   1629     public void setMaxHeight(int maxHeight) {
   1630         this.maxHeight = maxHeight;
   1631     }
   1632 
   1633     public int getHeight() {
   1634         if (height == INT_UNSET) {
   1635             return getParsedName().getHeight();
   1636         }
   1637         return height;
   1638     }
   1639 
   1640     public int getMaxHeight() {
   1641         return maxHeight;
   1642     }
   1643 
   1644     public int getOriginalHeight() {
   1645         return getParsedName().getHeight();
   1646     }
   1647 
   1648     public boolean isAbrEnabled() {
   1649         return false;
   1650     }
   1651 
   1652     public String getOriginalSize() {
   1653         if (width == INT_UNSET || height == INT_UNSET) {
   1654             return getParsedName().getSize();
   1655         }
   1656         return width + "x" + height;
   1657     }
   1658 
   1659     public String getDescription() {
   1660         return getParsedName().getDescription();
   1661     }
   1662 
   1663     public String toPrettyString() {
   1664         return getParsedName().toPrettyString();
   1665     }
   1666 
   1667     public AssetFileDescriptor getAssetFileDescriptor(Context context) {
   1668         try {
   1669             return context.getAssets().openFd(filename);
   1670         } catch (Exception e) {
   1671             e.printStackTrace();
   1672             return null;
   1673         }
   1674     }
   1675 
   1676 }
   1677 
   1678 /* File parser for filenames with format of {description}-{mimeType}_{size}_{framerate}.{format} */
   1679 class FilenameParser {
   1680 
   1681     static final String VP9 = "vp9";
   1682     static final String H264 = "h264";
   1683 
   1684     private final String filename;
   1685 
   1686     private String codec = VideoFormat.STRING_UNSET;
   1687     private String description = VideoFormat.STRING_UNSET;
   1688     private int width = VideoFormat.INT_UNSET;
   1689     private int height = VideoFormat.INT_UNSET;
   1690 
   1691     FilenameParser(String filename) {
   1692         this.filename = filename;
   1693         parseFilename(filename);
   1694     }
   1695 
   1696     public String getCodec() {
   1697         return codec;
   1698     }
   1699 
   1700     public String getMimeType() {
   1701         switch (codec) {
   1702             case H264:
   1703                 return MimeTypes.VIDEO_H264;
   1704             case VP9:
   1705                 return MimeTypes.VIDEO_VP9;
   1706             default:
   1707                 return null;
   1708         }
   1709     }
   1710 
   1711     public int getWidth() {
   1712         return width;
   1713     }
   1714 
   1715     public int getHeight() {
   1716         return height;
   1717     }
   1718 
   1719     public String getSize() {
   1720         return width + "x" + height;
   1721     }
   1722 
   1723     public String getDescription() {
   1724         return description;
   1725     }
   1726 
   1727     String toPrettyString() {
   1728         if (codec != null) {
   1729             return codec.toUpperCase() + " " + getSize();
   1730         }
   1731         return filename;
   1732     }
   1733 
   1734     private void parseFilename(String filename) {
   1735         final String descriptionDelimiter = "-";
   1736         final String infoDelimiter = "_";
   1737         final String sizeDelimiter = "x";
   1738         try {
   1739             this.description = filename.split(descriptionDelimiter)[0];
   1740             final String[] fileInfo = filename.split(descriptionDelimiter)[1].split(infoDelimiter);
   1741             this.codec = fileInfo[0];
   1742             this.width = Integer.parseInt(fileInfo[1].split(sizeDelimiter)[0]);
   1743             this.height = Integer.parseInt(fileInfo[1].split(sizeDelimiter)[1]);
   1744         } catch (Exception exception) { /* Filename format does not match. */ }
   1745     }
   1746 
   1747 }
   1748 
   1749 /**
   1750  * Compares bitmaps to determine if they are similar.
   1751  *
   1752  * <p>To determine greatest pixel difference we transform each pixel into the
   1753  * CIE L*a*b* color space. The euclidean distance formula is used to determine pixel differences.
   1754  */
   1755 class BitmapCompare {
   1756 
   1757     private static final int RED = 0;
   1758     private static final int GREEN = 1;
   1759     private static final int BLUE = 2;
   1760     private static final int X = 0;
   1761     private static final int Y = 1;
   1762     private static final int Z = 2;
   1763 
   1764     private BitmapCompare() {}
   1765 
   1766     /**
   1767      * Produces greatest pixel between two bitmaps. Used to determine bitmap similarity.
   1768      *
   1769      * @param bitmap1 A bitmap to compare to bitmap2.
   1770      * @param bitmap2 A bitmap to compare to bitmap1.
   1771      * @return A {@link Difference} with an integer describing the greatest pixel difference,
   1772      *     using {@link Integer#MAX_VALUE} for completely different bitmaps, and an optional
   1773      *     {@link Pair<Integer, Integer>} of the (col, row) pixel coordinate where it was first found.
   1774      */
   1775     @TargetApi(12)
   1776     public static Difference computeDifference(Bitmap bitmap1, Bitmap bitmap2) {
   1777         if (bitmap1 == null || bitmap2 == null) {
   1778             return new Difference(Integer.MAX_VALUE);
   1779         }
   1780         if (bitmap1.equals(bitmap2) || bitmap1.sameAs(bitmap2)) {
   1781             return new Difference(0);
   1782         }
   1783         if (bitmap1.getHeight() != bitmap2.getHeight() || bitmap1.getWidth() != bitmap2.getWidth()) {
   1784             return new Difference(Integer.MAX_VALUE);
   1785         }
   1786         // Convert all pixels to CIE L*a*b* color space so we can do a direct color comparison using
   1787         // euclidean distance formula.
   1788         final double[][] pixels1 = convertRgbToCieLab(bitmap1);
   1789         final double[][] pixels2 = convertRgbToCieLab(bitmap2);
   1790         int greatestDifference = 0;
   1791         int greatestDifferenceIndex = -1;
   1792         for (int i = 0; i < pixels1.length; i++) {
   1793             final int difference = euclideanDistance(pixels1[i], pixels2[i]);
   1794             if (difference > greatestDifference) {
   1795                 greatestDifference = difference;
   1796                 greatestDifferenceIndex = i;
   1797             }
   1798         }
   1799         return new Difference(greatestDifference, Pair.create(
   1800             greatestDifferenceIndex % bitmap1.getWidth(),
   1801             greatestDifferenceIndex / bitmap1.getHeight()));
   1802     }
   1803 
   1804     @SuppressLint("UseSparseArrays")
   1805     private static double[][] convertRgbToCieLab(Bitmap bitmap) {
   1806         final HashMap<Integer, double[]> pixelTransformCache = new HashMap<>();
   1807         final double[][] result = new double[bitmap.getHeight() * bitmap.getWidth()][3];
   1808         final int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()];
   1809         bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
   1810         for (int i = 0; i < pixels.length; i++) {
   1811             final double[] transformedColor = pixelTransformCache.get(pixels[i]);
   1812             if (transformedColor != null) {
   1813                 result[i] = transformedColor;
   1814             } else {
   1815                 result[i] = convertXyzToCieLab(convertRgbToXyz(pixels[i]));
   1816                 pixelTransformCache.put(pixels[i], result[i]);
   1817             }
   1818         }
   1819         return result;
   1820     }
   1821 
   1822     /**
   1823      * Conversion from RGB to XYZ based algorithm as defined by:
   1824      * http://www.easyrgb.com/index.php?X=MATH&H=02#text2
   1825      *
   1826      * <p><pre>{@code
   1827      *   var_R = ( R / 255 )        //R from 0 to 255
   1828      *   var_G = ( G / 255 )        //G from 0 to 255
   1829      *   var_B = ( B / 255 )        //B from 0 to 255
   1830      *
   1831      *   if ( var_R > 0.04045 ) var_R = ( ( var_R + 0.055 ) / 1.055 ) ^ 2.4
   1832      *   else                   var_R = var_R / 12.92
   1833      *   if ( var_G > 0.04045 ) var_G = ( ( var_G + 0.055 ) / 1.055 ) ^ 2.4
   1834      *   else                   var_G = var_G / 12.92
   1835      *   if ( var_B > 0.04045 ) var_B = ( ( var_B + 0.055 ) / 1.055 ) ^ 2.4
   1836      *   else                   var_B = var_B / 12.92
   1837      *
   1838      *   var_R = var_R * 100
   1839      *   var_G = var_G * 100
   1840      *   var_B = var_B * 100
   1841      *
   1842      *   // Observer. = 2, Illuminant = D65
   1843      *   X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
   1844      *   Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
   1845      *   Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
   1846      * }</pre>
   1847      *
   1848      * @param rgbColor A packed int made up of 4 bytes: alpha, red, green, blue.
   1849      * @return An array of doubles where each value is a component of the XYZ color space.
   1850      */
   1851     private static double[] convertRgbToXyz(int rgbColor) {
   1852         final double[] comp = {Color.red(rgbColor), Color.green(rgbColor), Color.blue(rgbColor)};
   1853         for (int i = 0; i < comp.length; i++) {
   1854             comp[i] /= 255.0;
   1855             if (comp[i] > 0.04045) {
   1856                 comp[i] = Math.pow((comp[i] + 0.055) / 1.055, 2.4);
   1857             } else {
   1858                 comp[i] /= 12.92;
   1859             }
   1860             comp[i] *= 100;
   1861         }
   1862         final double x = (comp[RED] * 0.4124) + (comp[GREEN] * 0.3576) + (comp[BLUE] * 0.1805);
   1863         final double y = (comp[RED] * 0.2126) + (comp[GREEN] * 0.7152) + (comp[BLUE] * 0.0722);
   1864         final double z = (comp[RED] * 0.0193) + (comp[GREEN] * 0.1192) + (comp[BLUE] * 0.9505);
   1865         return new double[] {x, y, z};
   1866     }
   1867 
   1868     /**
   1869      * Conversion from XYZ to CIE-L*a*b* based algorithm as defined by:
   1870      * http://www.easyrgb.com/index.php?X=MATH&H=07#text7
   1871      *
   1872      * <p><pre>
   1873      * {@code
   1874      *   var_X = X / ref_X          //ref_X =  95.047   Observer= 2, Illuminant= D65
   1875      *   var_Y = Y / ref_Y          //ref_Y = 100.000
   1876      *   var_Z = Z / ref_Z          //ref_Z = 108.883
   1877      *
   1878      *   if ( var_X > 0.008856 ) var_X = var_X ^ ( 1/3 )
   1879      *   else                    var_X = ( 7.787 * var_X ) + ( 16 / 116 )
   1880      *   if ( var_Y > 0.008856 ) var_Y = var_Y ^ ( 1/3 )
   1881      *   else                    var_Y = ( 7.787 * var_Y ) + ( 16 / 116 )
   1882      *   if ( var_Z > 0.008856 ) var_Z = var_Z ^ ( 1/3 )
   1883      *   else                    var_Z = ( 7.787 * var_Z ) + ( 16 / 116 )
   1884      *
   1885      *   CIE-L* = ( 116 * var_Y ) - 16
   1886      *   CIE-a* = 500 * ( var_X - var_Y )
   1887      *   CIE-b* = 200 * ( var_Y - var_Z )
   1888      * }
   1889      * </pre>
   1890      *
   1891      * @param comp An array of doubles where each value is a component of the XYZ color space.
   1892      * @return An array of doubles where each value is a component of the CIE-L*a*b* color space.
   1893      */
   1894     private static double[] convertXyzToCieLab(double[] comp) {
   1895         comp[X] /= 95.047;
   1896         comp[Y] /= 100.0;
   1897         comp[Z] /= 108.883;
   1898         for (int i = 0; i < comp.length; i++) {
   1899             if (comp[i] > 0.008856) {
   1900                 comp[i] = Math.pow(comp[i], (1.0 / 3.0));
   1901             } else {
   1902                 comp[i] = (7.787 * comp[i]) + (16.0 / 116.0);
   1903             }
   1904         }
   1905         final double l = (116 * comp[Y]) - 16;
   1906         final double a = 500 * (comp[X] - comp[Y]);
   1907         final double b = 200 * (comp[Y] - comp[Z]);
   1908         return new double[] {l, a, b};
   1909     }
   1910 
   1911     private static int euclideanDistance(double[] p1, double[] p2) {
   1912         if (p1.length != p2.length) {
   1913             return Integer.MAX_VALUE;
   1914         }
   1915         double result = 0;
   1916         for (int i = 0; i < p1.length; i++) {
   1917             result += Math.pow(p1[i] - p2[i], 2);
   1918         }
   1919         return (int) Math.round(Math.sqrt(result));
   1920     }
   1921 
   1922     /**
   1923      * Crops the border of the array representing an image by hBorderSize
   1924      * pixels on the left and right borders, and by vBorderSize pixels on the
   1925      * top and bottom borders (so the width is 2 * hBorderSize smaller and
   1926      * the height is 2 * vBorderSize smaller), then scales the image up to
   1927      * match the original size using bilinear interpolation.
   1928      */
   1929     private static Bitmap shrinkAndScaleBilinear(
   1930             Bitmap input, double hBorderSize, double vBorderSize) {
   1931 
   1932         int width = input.getWidth();
   1933         int height = input.getHeight();
   1934 
   1935         // Compute the proper step sizes
   1936         double xInc = ((double) width - 1 - hBorderSize * 2) / (double) (width - 1);
   1937         double yInc = ((double) height - 1 - vBorderSize * 2) / (double) (height - 1);
   1938 
   1939         // Read the input bitmap into RGB arrays.
   1940         int[] inputPixels = new int[width * height];
   1941         input.getPixels(inputPixels, 0, width, 0, 0, width, height);
   1942         int[][] inputRgb = new int[width * height][3];
   1943         for (int i = 0; i < width * height; ++i) {
   1944             inputRgb[i][0] = Color.red(inputPixels[i]);
   1945             inputRgb[i][1] = Color.green(inputPixels[i]);
   1946             inputRgb[i][2] = Color.blue(inputPixels[i]);
   1947         }
   1948         inputPixels = null;
   1949 
   1950         // Prepare the output buffer.
   1951         int[] outputPixels = new int[width * height];
   1952 
   1953         // Start the iteration. The first y coordinate is vBorderSize.
   1954         double y = vBorderSize;
   1955         for (int yIndex = 0; yIndex < height; ++yIndex) {
   1956             // The first x coordinate is hBorderSize.
   1957             double x = hBorderSize;
   1958             for (int xIndex = 0; xIndex < width; ++xIndex) {
   1959                 // Determine the square of interest.
   1960                 int left = (int)x;    // This is floor(x).
   1961                 int top = (int)y;     // This is floor(y).
   1962                 int right = left + 1;
   1963                 int bottom = top + 1;
   1964 
   1965                 // (u, v) is the fractional part of (x, y).
   1966                 double u = x - (double)left;
   1967                 double v = y - (double)top;
   1968 
   1969                 // Precompute necessary products to save time.
   1970                 double p00 = (1.0 - u) * (1.0 - v);
   1971                 double p01 = (1.0 - u) * v;
   1972                 double p10 = u * (1.0 - v);
   1973                 double p11 = u * v;
   1974 
   1975                 // Clamp the indices to prevent out-of-bound that may be caused
   1976                 // by round-off error.
   1977                 if (left >= width) left = width - 1;
   1978                 if (top >= height) top = height - 1;
   1979                 if (right >= width) right = width - 1;
   1980                 if (bottom >= height) bottom = height - 1;
   1981 
   1982                 // Sample RGB values from the four corners.
   1983                 int[] rgb00 = inputRgb[top * width + left];
   1984                 int[] rgb01 = inputRgb[bottom * width + left];
   1985                 int[] rgb10 = inputRgb[top * width + right];
   1986                 int[] rgb11 = inputRgb[bottom * width + right];
   1987 
   1988                 // Interpolate each component of RGB separately.
   1989                 int[] mixedColor = new int[3];
   1990                 for (int k = 0; k < 3; ++k) {
   1991                     mixedColor[k] = (int)Math.round(
   1992                             p00 * (double) rgb00[k] + p01 * (double) rgb01[k]
   1993                             + p10 * (double) rgb10[k] + p11 * (double) rgb11[k]);
   1994                 }
   1995                 // Convert RGB to bitmap Color format and store.
   1996                 outputPixels[yIndex * width + xIndex] = Color.rgb(
   1997                         mixedColor[0], mixedColor[1], mixedColor[2]);
   1998                 x += xInc;
   1999             }
   2000             y += yInc;
   2001         }
   2002         // Assemble the output buffer into a Bitmap object.
   2003         return Bitmap.createBitmap(outputPixels, width, height, input.getConfig());
   2004     }
   2005 
   2006     /**
   2007      * Calls computeDifference on multiple cropped-and-scaled versions of
   2008      * bitmap2.
   2009      */
   2010     @TargetApi(12)
   2011     public static Difference computeMinimumDifference(
   2012             Bitmap bitmap1, Bitmap bitmap2, Pair<Double, Double>[] borderCrops) {
   2013 
   2014         // Compute the difference with the original image (bitmap2) first.
   2015         Difference minDiff = computeDifference(bitmap1, bitmap2);
   2016         // Then go through the list of borderCrops.
   2017         for (Pair<Double, Double> borderCrop : borderCrops) {
   2018             // Compute the difference between bitmap1 and a transformed
   2019             // version of bitmap2.
   2020             Bitmap bitmap2s = shrinkAndScaleBilinear(bitmap2, borderCrop.first, borderCrop.second);
   2021             Difference d = computeDifference(bitmap1, bitmap2s);
   2022             // Keep the minimum difference.
   2023             if (d.greatestPixelDifference < minDiff.greatestPixelDifference) {
   2024                 minDiff = d;
   2025                 minDiff.bestMatchBorderCrop = borderCrop;
   2026             }
   2027         }
   2028         return minDiff;
   2029     }
   2030 
   2031     /**
   2032      * Calls computeMinimumDifference on a default list of borderCrop.
   2033      */
   2034     @TargetApi(12)
   2035     public static Difference computeMinimumDifference(
   2036             Bitmap bitmap1, Bitmap bitmap2, int trueWidth, int trueHeight) {
   2037 
   2038         double hBorder = (double) bitmap1.getWidth() / (double) trueWidth;
   2039         double vBorder = (double) bitmap1.getHeight() / (double) trueHeight;
   2040         double hBorderH = 0.5 * hBorder; // Half-texel horizontal border
   2041         double vBorderH = 0.5 * vBorder; // Half-texel vertical border
   2042         return computeMinimumDifference(
   2043                 bitmap1,
   2044                 bitmap2,
   2045                 new Pair[] {
   2046                     Pair.create(hBorderH, 0.0),
   2047                     Pair.create(hBorderH, vBorderH),
   2048                     Pair.create(0.0, vBorderH),
   2049                     Pair.create(hBorder, 0.0),
   2050                     Pair.create(hBorder, vBorder),
   2051                     Pair.create(0.0, vBorder)
   2052                 });
   2053         // This default list of borderCrop comes from the behavior of
   2054         // GLConsumer.computeTransformMatrix().
   2055     }
   2056 
   2057     /* Describes the difference between two {@link Bitmap} instances. */
   2058     public static final class Difference {
   2059 
   2060         public final int greatestPixelDifference;
   2061         public final Pair<Integer, Integer> greatestPixelDifferenceCoordinates;
   2062         public Pair<Double, Double> bestMatchBorderCrop;
   2063 
   2064         private Difference(int greatestPixelDifference) {
   2065             this(greatestPixelDifference, null, Pair.create(0.0, 0.0));
   2066         }
   2067 
   2068         private Difference(
   2069                 int greatestPixelDifference,
   2070                 Pair<Integer, Integer> greatestPixelDifferenceCoordinates) {
   2071             this(greatestPixelDifference, greatestPixelDifferenceCoordinates,
   2072                     Pair.create(0.0, 0.0));
   2073         }
   2074 
   2075         private Difference(
   2076                 int greatestPixelDifference,
   2077                 Pair<Integer, Integer> greatestPixelDifferenceCoordinates,
   2078                 Pair<Double, Double> bestMatchBorderCrop) {
   2079             this.greatestPixelDifference = greatestPixelDifference;
   2080             this.greatestPixelDifferenceCoordinates = greatestPixelDifferenceCoordinates;
   2081             this.bestMatchBorderCrop = bestMatchBorderCrop;
   2082         }
   2083     }
   2084 
   2085 }
   2086 
   2087 /* Wrapper for MIME types. */
   2088 final class MimeTypes {
   2089 
   2090     private MimeTypes() {}
   2091 
   2092     public static final String VIDEO_VP9 = "video/x-vnd.on2.vp9";
   2093     public static final String VIDEO_H264 = "video/avc";
   2094 
   2095     public static boolean isVideo(String mimeType) {
   2096         return mimeType.startsWith("video");
   2097     }
   2098 
   2099 }
   2100