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