• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.google.android.exoplayer2.testutil;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 import static org.junit.Assert.fail;
20 
21 import android.content.Context;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.database.sqlite.SQLiteOpenHelper;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Color;
27 import android.media.MediaCodec;
28 import android.net.Uri;
29 import com.google.android.exoplayer2.C;
30 import com.google.android.exoplayer2.database.DatabaseProvider;
31 import com.google.android.exoplayer2.database.DefaultDatabaseProvider;
32 import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
33 import com.google.android.exoplayer2.extractor.Extractor;
34 import com.google.android.exoplayer2.extractor.ExtractorInput;
35 import com.google.android.exoplayer2.extractor.PositionHolder;
36 import com.google.android.exoplayer2.extractor.SeekMap;
37 import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
38 import com.google.android.exoplayer2.upstream.DataSource;
39 import com.google.android.exoplayer2.upstream.DataSpec;
40 import com.google.android.exoplayer2.util.Assertions;
41 import com.google.android.exoplayer2.util.Util;
42 import java.io.File;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.nio.ByteBuffer;
47 import java.util.Arrays;
48 import java.util.Random;
49 
50 /**
51  * Utility methods for tests.
52  */
53 public class TestUtil {
54 
TestUtil()55   private TestUtil() {}
56 
57   /**
58    * Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)}
59    * until {@link C#RESULT_END_OF_INPUT} is returned.
60    *
61    * @param dataSource The source from which to read.
62    * @return The concatenation of all read data.
63    * @throws IOException If an error occurs reading from the source.
64    */
readToEnd(DataSource dataSource)65   public static byte[] readToEnd(DataSource dataSource) throws IOException {
66     byte[] data = new byte[1024];
67     int position = 0;
68     int bytesRead = 0;
69     while (bytesRead != C.RESULT_END_OF_INPUT) {
70       if (position == data.length) {
71         data = Arrays.copyOf(data, data.length * 2);
72       }
73       bytesRead = dataSource.read(data, position, data.length - position);
74       if (bytesRead != C.RESULT_END_OF_INPUT) {
75         position += bytesRead;
76       }
77     }
78     return Arrays.copyOf(data, position);
79   }
80 
81   /**
82    * Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)}
83    * until exactly {@code length} bytes have been read.
84    *
85    * @param dataSource The source from which to read.
86    * @return The read data.
87    * @throws IOException If an error occurs reading from the source.
88    */
readExactly(DataSource dataSource, int length)89   public static byte[] readExactly(DataSource dataSource, int length) throws IOException {
90     byte[] data = new byte[length];
91     int position = 0;
92     while (position < length) {
93       int bytesRead = dataSource.read(data, position, data.length - position);
94       if (bytesRead == C.RESULT_END_OF_INPUT) {
95         fail("Not enough data could be read: " + position + " < " + length);
96       } else {
97         position += bytesRead;
98       }
99     }
100     return data;
101   }
102 
103   /**
104    * Equivalent to {@code buildTestData(length, length)}.
105    *
106    * @param length The length of the array.
107    * @return The generated array.
108    */
buildTestData(int length)109   public static byte[] buildTestData(int length) {
110     return buildTestData(length, length);
111   }
112 
113   /**
114    * Generates an array of random bytes with the specified length.
115    *
116    * @param length The length of the array.
117    * @param seed A seed for an internally created {@link Random source of randomness}.
118    * @return The generated array.
119    */
buildTestData(int length, int seed)120   public static byte[] buildTestData(int length, int seed) {
121     return buildTestData(length, new Random(seed));
122   }
123 
124   /**
125    * Generates an array of random bytes with the specified length.
126    *
127    * @param length The length of the array.
128    * @param random A source of randomness.
129    * @return The generated array.
130    */
buildTestData(int length, Random random)131   public static byte[] buildTestData(int length, Random random) {
132     byte[] source = new byte[length];
133     random.nextBytes(source);
134     return source;
135   }
136 
137   /**
138    * Generates a random string with the specified maximum length.
139    *
140    * @param maximumLength The maximum length of the string.
141    * @param random A source of randomness.
142    * @return The generated string.
143    */
buildTestString(int maximumLength, Random random)144   public static String buildTestString(int maximumLength, Random random) {
145     int length = random.nextInt(maximumLength);
146     StringBuilder builder = new StringBuilder(length);
147     for (int i = 0; i < length; i++) {
148       builder.append((char) random.nextInt());
149     }
150     return builder.toString();
151   }
152 
153   /**
154    * Converts an array of integers in the range [0, 255] into an equivalent byte array.
155    *
156    * @param intArray An array of integers, all of which must be in the range [0, 255].
157    * @return The equivalent byte array.
158    */
createByteArray(int... intArray)159   public static byte[] createByteArray(int... intArray) {
160     byte[] byteArray = new byte[intArray.length];
161     for (int i = 0; i < byteArray.length; i++) {
162       Assertions.checkState(0x00 <= intArray[i] && intArray[i] <= 0xFF);
163       byteArray[i] = (byte) intArray[i];
164     }
165     return byteArray;
166   }
167 
168   /**
169    * Concatenates the provided byte arrays.
170    *
171    * @param byteArrays The byte arrays to concatenate.
172    * @return The concatenated result.
173    */
joinByteArrays(byte[]... byteArrays)174   public static byte[] joinByteArrays(byte[]... byteArrays) {
175     int length = 0;
176     for (byte[] byteArray : byteArrays) {
177       length += byteArray.length;
178     }
179     byte[] joined = new byte[length];
180     length = 0;
181     for (byte[] byteArray : byteArrays) {
182       System.arraycopy(byteArray, 0, joined, length, byteArray.length);
183       length += byteArray.length;
184     }
185     return joined;
186   }
187 
188   /** Writes one byte long dummy test data to the file and returns it. */
createTestFile(File directory, String name)189   public static File createTestFile(File directory, String name) throws IOException {
190     return createTestFile(directory, name, /* length= */ 1);
191   }
192 
193   /** Writes dummy test data with the specified length to the file and returns it. */
createTestFile(File directory, String name, long length)194   public static File createTestFile(File directory, String name, long length) throws IOException {
195     return createTestFile(new File(directory, name), length);
196   }
197 
198   /** Writes dummy test data with the specified length to the file and returns it. */
createTestFile(File file, long length)199   public static File createTestFile(File file, long length) throws IOException {
200     FileOutputStream output = new FileOutputStream(file);
201     for (long i = 0; i < length; i++) {
202       output.write((int) i);
203     }
204     output.close();
205     return file;
206   }
207 
208   /** Returns the bytes of an asset file. */
getByteArray(Context context, String fileName)209   public static byte[] getByteArray(Context context, String fileName) throws IOException {
210     return Util.toByteArray(getInputStream(context, fileName));
211   }
212 
213   /** Returns an {@link InputStream} for reading from an asset file. */
getInputStream(Context context, String fileName)214   public static InputStream getInputStream(Context context, String fileName) throws IOException {
215     return context.getResources().getAssets().open(fileName);
216   }
217 
218   /** Returns a {@link String} read from an asset file. */
getString(Context context, String fileName)219   public static String getString(Context context, String fileName) throws IOException {
220     return Util.fromUtf8Bytes(getByteArray(context, fileName));
221   }
222 
223   /** Returns a {@link Bitmap} read from an asset file. */
getBitmap(Context context, String fileName)224   public static Bitmap getBitmap(Context context, String fileName) throws IOException {
225     return BitmapFactory.decodeStream(getInputStream(context, fileName));
226   }
227 
228   /** Returns a {@link DatabaseProvider} that provides an in-memory database. */
getInMemoryDatabaseProvider()229   public static DatabaseProvider getInMemoryDatabaseProvider() {
230     return new DefaultDatabaseProvider(
231         new SQLiteOpenHelper(
232             /* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) {
233           @Override
234           public void onCreate(SQLiteDatabase db) {
235             // Do nothing.
236           }
237 
238           @Override
239           public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
240             // Do nothing.
241           }
242         });
243   }
244 
245   /**
246    * Asserts that data read from a {@link DataSource} matches {@code expected}.
247    *
248    * @param dataSource The {@link DataSource} through which to read.
249    * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}.
250    * @param expectedData The expected data.
251    * @param expectKnownLength Whether to assert that {@link DataSource#open} returns the expected
252    *     data length. If false then it's asserted that {@link C#LENGTH_UNSET} is returned.
253    * @throws IOException If an error occurs reading fom the {@link DataSource}.
254    */
assertDataSourceContent( DataSource dataSource, DataSpec dataSpec, byte[] expectedData, boolean expectKnownLength)255   public static void assertDataSourceContent(
256       DataSource dataSource, DataSpec dataSpec, byte[] expectedData, boolean expectKnownLength)
257       throws IOException {
258     try {
259       long length = dataSource.open(dataSpec);
260       assertThat(length).isEqualTo(expectKnownLength ? expectedData.length : C.LENGTH_UNSET);
261       byte[] readData = readToEnd(dataSource);
262       assertThat(readData).isEqualTo(expectedData);
263     } finally {
264       dataSource.close();
265     }
266   }
267 
268   /** Returns whether two {@link android.media.MediaCodec.BufferInfo BufferInfos} are equal. */
assertBufferInfosEqual( MediaCodec.BufferInfo expected, MediaCodec.BufferInfo actual)269   public static void assertBufferInfosEqual(
270       MediaCodec.BufferInfo expected, MediaCodec.BufferInfo actual) {
271     assertThat(expected.flags).isEqualTo(actual.flags);
272     assertThat(expected.offset).isEqualTo(actual.offset);
273     assertThat(expected.presentationTimeUs).isEqualTo(actual.presentationTimeUs);
274     assertThat(expected.size).isEqualTo(actual.size);
275   }
276 
277   /**
278    * Asserts whether actual bitmap is very similar to the expected bitmap at some quality level.
279    *
280    * <p>This is defined as their PSNR value is greater than or equal to the threshold. The higher
281    * the threshold, the more similar they are.
282    *
283    * @param expectedBitmap The expected bitmap.
284    * @param actualBitmap The actual bitmap.
285    * @param psnrThresholdDb The PSNR threshold (in dB), at or above which bitmaps are considered
286    *     very similar.
287    */
assertBitmapsAreSimilar( Bitmap expectedBitmap, Bitmap actualBitmap, double psnrThresholdDb)288   public static void assertBitmapsAreSimilar(
289       Bitmap expectedBitmap, Bitmap actualBitmap, double psnrThresholdDb) {
290     assertThat(getPsnr(expectedBitmap, actualBitmap)).isAtLeast(psnrThresholdDb);
291   }
292 
293   /**
294    * Calculates the Peak-Signal-to-Noise-Ratio value for 2 bitmaps.
295    *
296    * <p>This is the logarithmic decibel(dB) value of the average mean-squared-error of normalized
297    * (0.0-1.0) R/G/B values from the two bitmaps. The higher the value, the more similar they are.
298    *
299    * @param firstBitmap The first bitmap.
300    * @param secondBitmap The second bitmap.
301    * @return The PSNR value calculated from these 2 bitmaps.
302    */
getPsnr(Bitmap firstBitmap, Bitmap secondBitmap)303   private static double getPsnr(Bitmap firstBitmap, Bitmap secondBitmap) {
304     assertThat(firstBitmap.getWidth()).isEqualTo(secondBitmap.getWidth());
305     assertThat(firstBitmap.getHeight()).isEqualTo(secondBitmap.getHeight());
306     long mse = 0;
307     for (int i = 0; i < firstBitmap.getWidth(); i++) {
308       for (int j = 0; j < firstBitmap.getHeight(); j++) {
309         int firstColorInt = firstBitmap.getPixel(i, j);
310         int firstRed = Color.red(firstColorInt);
311         int firstGreen = Color.green(firstColorInt);
312         int firstBlue = Color.blue(firstColorInt);
313         int secondColorInt = secondBitmap.getPixel(i, j);
314         int secondRed = Color.red(secondColorInt);
315         int secondGreen = Color.green(secondColorInt);
316         int secondBlue = Color.blue(secondColorInt);
317         mse +=
318             ((firstRed - secondRed) * (firstRed - secondRed)
319                 + (firstGreen - secondGreen) * (firstGreen - secondGreen)
320                 + (firstBlue - secondBlue) * (firstBlue - secondBlue));
321       }
322     }
323     double normalizedMse =
324         mse / (255.0 * 255.0 * 3.0 * firstBitmap.getWidth() * firstBitmap.getHeight());
325     return 10 * Math.log10(1.0 / normalizedMse);
326   }
327 
328   /** Returns the {@link Uri} for the given asset path. */
buildAssetUri(String assetPath)329   public static Uri buildAssetUri(String assetPath) {
330     return Uri.parse("asset:///" + assetPath);
331   }
332 
333   /**
334    * Reads from the given input using the given {@link Extractor}, until it can produce the {@link
335    * SeekMap} and all of the tracks have been identified, or until the extractor encounters EOF.
336    *
337    * @param extractor The {@link Extractor} to extractor from input.
338    * @param output The {@link FakeTrackOutput} to store the extracted {@link SeekMap} and track.
339    * @param dataSource The {@link DataSource} that will be used to read from the input.
340    * @param uri The Uri of the input.
341    * @return The extracted {@link SeekMap}.
342    * @throws IOException If an error occurred reading from the input, or if the extractor finishes
343    *     reading from input without extracting any {@link SeekMap}.
344    */
extractSeekMap( Extractor extractor, FakeExtractorOutput output, DataSource dataSource, Uri uri)345   public static SeekMap extractSeekMap(
346       Extractor extractor, FakeExtractorOutput output, DataSource dataSource, Uri uri)
347       throws IOException {
348     ExtractorInput input = getExtractorInputFromPosition(dataSource, /* position= */ 0, uri);
349     extractor.init(output);
350     PositionHolder positionHolder = new PositionHolder();
351     int readResult = Extractor.RESULT_CONTINUE;
352     while (true) {
353       try {
354         // Keep reading until we can get the seek map
355         while (readResult == Extractor.RESULT_CONTINUE
356             && (output.seekMap == null || !output.tracksEnded)) {
357           readResult = extractor.read(input, positionHolder);
358         }
359       } finally {
360         Util.closeQuietly(dataSource);
361       }
362 
363       if (readResult == Extractor.RESULT_SEEK) {
364         input = getExtractorInputFromPosition(dataSource, positionHolder.position, uri);
365         readResult = Extractor.RESULT_CONTINUE;
366       } else if (readResult == Extractor.RESULT_END_OF_INPUT) {
367         throw new IOException("EOF encountered without seekmap");
368       }
369       if (output.seekMap != null) {
370         return output.seekMap;
371       }
372     }
373   }
374 
375   /**
376    * Extracts all samples from the given file into a {@link FakeTrackOutput}.
377    *
378    * @param extractor The {@link Extractor} to extractor from input.
379    * @param context A {@link Context}.
380    * @param fileName The name of the input file.
381    * @return The {@link FakeTrackOutput} containing the extracted samples.
382    * @throws IOException If an error occurred reading from the input, or if the extractor finishes
383    *     reading from input without extracting any {@link SeekMap}.
384    */
extractAllSamplesFromFile( Extractor extractor, Context context, String fileName)385   public static FakeExtractorOutput extractAllSamplesFromFile(
386       Extractor extractor, Context context, String fileName) throws IOException {
387     byte[] data = TestUtil.getByteArray(context, fileName);
388     FakeExtractorOutput expectedOutput = new FakeExtractorOutput();
389     extractor.init(expectedOutput);
390     FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
391 
392     PositionHolder positionHolder = new PositionHolder();
393     int readResult = Extractor.RESULT_CONTINUE;
394     while (readResult != Extractor.RESULT_END_OF_INPUT) {
395       while (readResult == Extractor.RESULT_CONTINUE) {
396         readResult = extractor.read(input, positionHolder);
397       }
398       if (readResult == Extractor.RESULT_SEEK) {
399         input.setPosition((int) positionHolder.position);
400         readResult = Extractor.RESULT_CONTINUE;
401       }
402     }
403     return expectedOutput;
404   }
405 
406   /**
407    * Seeks to the given seek time of the stream from the given input, and keeps reading from the
408    * input until we can extract at least one sample following the seek position, or until
409    * end-of-input is reached.
410    *
411    * @param extractor The {@link Extractor} to extract from input.
412    * @param seekMap The {@link SeekMap} of the stream from the given input.
413    * @param seekTimeUs The seek time, in micro-seconds.
414    * @param trackOutput The {@link FakeTrackOutput} to store the extracted samples.
415    * @param dataSource The {@link DataSource} that will be used to read from the input.
416    * @param uri The Uri of the input.
417    * @return The index of the first extracted sample written to the given {@code trackOutput} after
418    *     the seek is completed, or {@link C#INDEX_UNSET} if the seek is completed without any
419    *     extracted sample.
420    */
seekToTimeUs( Extractor extractor, SeekMap seekMap, long seekTimeUs, DataSource dataSource, FakeTrackOutput trackOutput, Uri uri)421   public static int seekToTimeUs(
422       Extractor extractor,
423       SeekMap seekMap,
424       long seekTimeUs,
425       DataSource dataSource,
426       FakeTrackOutput trackOutput,
427       Uri uri)
428       throws IOException {
429     int numSampleBeforeSeek = trackOutput.getSampleCount();
430     SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
431 
432     long initialSeekLoadPosition = seekPoints.first.position;
433     extractor.seek(initialSeekLoadPosition, seekTimeUs);
434 
435     PositionHolder positionHolder = new PositionHolder();
436     positionHolder.position = C.POSITION_UNSET;
437     ExtractorInput extractorInput =
438         TestUtil.getExtractorInputFromPosition(dataSource, initialSeekLoadPosition, uri);
439     int extractorReadResult = Extractor.RESULT_CONTINUE;
440     while (true) {
441       try {
442         // Keep reading until we can read at least one sample after seek
443         while (extractorReadResult == Extractor.RESULT_CONTINUE
444             && trackOutput.getSampleCount() == numSampleBeforeSeek) {
445           extractorReadResult = extractor.read(extractorInput, positionHolder);
446         }
447       } finally {
448         Util.closeQuietly(dataSource);
449       }
450 
451       if (extractorReadResult == Extractor.RESULT_SEEK) {
452         extractorInput =
453             TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, uri);
454         extractorReadResult = Extractor.RESULT_CONTINUE;
455       } else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT
456           && trackOutput.getSampleCount() == numSampleBeforeSeek) {
457         return C.INDEX_UNSET;
458       } else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
459         // First index after seek = num sample before seek.
460         return numSampleBeforeSeek;
461       }
462     }
463   }
464 
465   /** Returns an {@link ExtractorInput} to read from the given input at given position. */
getExtractorInputFromPosition( DataSource dataSource, long position, Uri uri)466   public static ExtractorInput getExtractorInputFromPosition(
467       DataSource dataSource, long position, Uri uri) throws IOException {
468     DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET);
469     long length = dataSource.open(dataSpec);
470     if (length != C.LENGTH_UNSET) {
471       length += position;
472     }
473     return new DefaultExtractorInput(dataSource, position, length);
474   }
475 
476   /**
477    * Create a new {@link MetadataInputBuffer} and copy {@code data} into the backing {@link
478    * ByteBuffer}.
479    */
createMetadataInputBuffer(byte[] data)480   public static MetadataInputBuffer createMetadataInputBuffer(byte[] data) {
481     MetadataInputBuffer buffer = new MetadataInputBuffer();
482     buffer.data = ByteBuffer.allocate(data.length).put(data);
483     buffer.data.flip();
484     return buffer;
485   }
486 }
487