blob: a86fe072336d3ddd8ee616746bf24d8844096889 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.MediaCodec;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.database.DatabaseProvider;
import com.google.android.exoplayer2.database.DefaultDatabaseProvider;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
/**
* Utility methods for tests.
*/
public class TestUtil {
private TestUtil() {}
/**
* Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)}
* until {@link C#RESULT_END_OF_INPUT} is returned.
*
* @param dataSource The source from which to read.
* @return The concatenation of all read data.
* @throws IOException If an error occurs reading from the source.
*/
public static byte[] readToEnd(DataSource dataSource) throws IOException {
byte[] data = new byte[1024];
int position = 0;
int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) {
if (position == data.length) {
data = Arrays.copyOf(data, data.length * 2);
}
bytesRead = dataSource.read(data, position, data.length - position);
if (bytesRead != C.RESULT_END_OF_INPUT) {
position += bytesRead;
}
}
return Arrays.copyOf(data, position);
}
/**
* Given an open {@link DataSource}, repeatedly calls {@link DataSource#read(byte[], int, int)}
* until exactly {@code length} bytes have been read.
*
* @param dataSource The source from which to read.
* @return The read data.
* @throws IOException If an error occurs reading from the source.
*/
public static byte[] readExactly(DataSource dataSource, int length) throws IOException {
byte[] data = new byte[length];
int position = 0;
while (position < length) {
int bytesRead = dataSource.read(data, position, data.length - position);
if (bytesRead == C.RESULT_END_OF_INPUT) {
fail("Not enough data could be read: " + position + " < " + length);
} else {
position += bytesRead;
}
}
return data;
}
/**
* Equivalent to {@code buildTestData(length, length)}.
*
* @param length The length of the array.
* @return The generated array.
*/
public static byte[] buildTestData(int length) {
return buildTestData(length, length);
}
/**
* Generates an array of random bytes with the specified length.
*
* @param length The length of the array.
* @param seed A seed for an internally created {@link Random source of randomness}.
* @return The generated array.
*/
public static byte[] buildTestData(int length, int seed) {
return buildTestData(length, new Random(seed));
}
/**
* Generates an array of random bytes with the specified length.
*
* @param length The length of the array.
* @param random A source of randomness.
* @return The generated array.
*/
public static byte[] buildTestData(int length, Random random) {
byte[] source = new byte[length];
random.nextBytes(source);
return source;
}
/**
* Generates a random string with the specified maximum length.
*
* @param maximumLength The maximum length of the string.
* @param random A source of randomness.
* @return The generated string.
*/
public static String buildTestString(int maximumLength, Random random) {
int length = random.nextInt(maximumLength);
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
builder.append((char) random.nextInt());
}
return builder.toString();
}
/**
* Converts an array of integers in the range [0, 255] into an equivalent byte array.
*
* @param intArray An array of integers, all of which must be in the range [0, 255].
* @return The equivalent byte array.
*/
public static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
Assertions.checkState(0x00 <= intArray[i] && intArray[i] <= 0xFF);
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
/**
* Concatenates the provided byte arrays.
*
* @param byteArrays The byte arrays to concatenate.
* @return The concatenated result.
*/
public static byte[] joinByteArrays(byte[]... byteArrays) {
int length = 0;
for (byte[] byteArray : byteArrays) {
length += byteArray.length;
}
byte[] joined = new byte[length];
length = 0;
for (byte[] byteArray : byteArrays) {
System.arraycopy(byteArray, 0, joined, length, byteArray.length);
length += byteArray.length;
}
return joined;
}
/** Writes one byte long dummy test data to the file and returns it. */
public static File createTestFile(File directory, String name) throws IOException {
return createTestFile(directory, name, /* length= */ 1);
}
/** Writes dummy test data with the specified length to the file and returns it. */
public static File createTestFile(File directory, String name, long length) throws IOException {
return createTestFile(new File(directory, name), length);
}
/** Writes dummy test data with the specified length to the file and returns it. */
public static File createTestFile(File file, long length) throws IOException {
FileOutputStream output = new FileOutputStream(file);
for (long i = 0; i < length; i++) {
output.write((int) i);
}
output.close();
return file;
}
/** Returns the bytes of an asset file. */
public static byte[] getByteArray(Context context, String fileName) throws IOException {
return Util.toByteArray(getInputStream(context, fileName));
}
/** Returns an {@link InputStream} for reading from an asset file. */
public static InputStream getInputStream(Context context, String fileName) throws IOException {
return context.getResources().getAssets().open(fileName);
}
/** Returns a {@link String} read from an asset file. */
public static String getString(Context context, String fileName) throws IOException {
return Util.fromUtf8Bytes(getByteArray(context, fileName));
}
/** Returns a {@link Bitmap} read from an asset file. */
public static Bitmap getBitmap(Context context, String fileName) throws IOException {
return BitmapFactory.decodeStream(getInputStream(context, fileName));
}
/** Returns a {@link DatabaseProvider} that provides an in-memory database. */
public static DatabaseProvider getInMemoryDatabaseProvider() {
return new DefaultDatabaseProvider(
new SQLiteOpenHelper(
/* context= */ null, /* name= */ null, /* factory= */ null, /* version= */ 1) {
@Override
public void onCreate(SQLiteDatabase db) {
// Do nothing.
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Do nothing.
}
});
}
/**
* Asserts that data read from a {@link DataSource} matches {@code expected}.
*
* @param dataSource The {@link DataSource} through which to read.
* @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}.
* @param expectedData The expected data.
* @param expectKnownLength Whether to assert that {@link DataSource#open} returns the expected
* data length. If false then it's asserted that {@link C#LENGTH_UNSET} is returned.
* @throws IOException If an error occurs reading fom the {@link DataSource}.
*/
public static void assertDataSourceContent(
DataSource dataSource, DataSpec dataSpec, byte[] expectedData, boolean expectKnownLength)
throws IOException {
try {
long length = dataSource.open(dataSpec);
assertThat(length).isEqualTo(expectKnownLength ? expectedData.length : C.LENGTH_UNSET);
byte[] readData = readToEnd(dataSource);
assertThat(readData).isEqualTo(expectedData);
} finally {
dataSource.close();
}
}
/** Returns whether two {@link android.media.MediaCodec.BufferInfo BufferInfos} are equal. */
public static void assertBufferInfosEqual(
MediaCodec.BufferInfo expected, MediaCodec.BufferInfo actual) {
assertThat(expected.flags).isEqualTo(actual.flags);
assertThat(expected.offset).isEqualTo(actual.offset);
assertThat(expected.presentationTimeUs).isEqualTo(actual.presentationTimeUs);
assertThat(expected.size).isEqualTo(actual.size);
}
/**
* Asserts whether actual bitmap is very similar to the expected bitmap at some quality level.
*
* <p>This is defined as their PSNR value is greater than or equal to the threshold. The higher
* the threshold, the more similar they are.
*
* @param expectedBitmap The expected bitmap.
* @param actualBitmap The actual bitmap.
* @param psnrThresholdDb The PSNR threshold (in dB), at or above which bitmaps are considered
* very similar.
*/
public static void assertBitmapsAreSimilar(
Bitmap expectedBitmap, Bitmap actualBitmap, double psnrThresholdDb) {
assertThat(getPsnr(expectedBitmap, actualBitmap)).isAtLeast(psnrThresholdDb);
}
/**
* Calculates the Peak-Signal-to-Noise-Ratio value for 2 bitmaps.
*
* <p>This is the logarithmic decibel(dB) value of the average mean-squared-error of normalized
* (0.0-1.0) R/G/B values from the two bitmaps. The higher the value, the more similar they are.
*
* @param firstBitmap The first bitmap.
* @param secondBitmap The second bitmap.
* @return The PSNR value calculated from these 2 bitmaps.
*/
private static double getPsnr(Bitmap firstBitmap, Bitmap secondBitmap) {
assertThat(firstBitmap.getWidth()).isEqualTo(secondBitmap.getWidth());
assertThat(firstBitmap.getHeight()).isEqualTo(secondBitmap.getHeight());
long mse = 0;
for (int i = 0; i < firstBitmap.getWidth(); i++) {
for (int j = 0; j < firstBitmap.getHeight(); j++) {
int firstColorInt = firstBitmap.getPixel(i, j);
int firstRed = Color.red(firstColorInt);
int firstGreen = Color.green(firstColorInt);
int firstBlue = Color.blue(firstColorInt);
int secondColorInt = secondBitmap.getPixel(i, j);
int secondRed = Color.red(secondColorInt);
int secondGreen = Color.green(secondColorInt);
int secondBlue = Color.blue(secondColorInt);
mse +=
((firstRed - secondRed) * (firstRed - secondRed)
+ (firstGreen - secondGreen) * (firstGreen - secondGreen)
+ (firstBlue - secondBlue) * (firstBlue - secondBlue));
}
}
double normalizedMse =
mse / (255.0 * 255.0 * 3.0 * firstBitmap.getWidth() * firstBitmap.getHeight());
return 10 * Math.log10(1.0 / normalizedMse);
}
/** Returns the {@link Uri} for the given asset path. */
public static Uri buildAssetUri(String assetPath) {
return Uri.parse("asset:///" + assetPath);
}
/**
* Reads from the given input using the given {@link Extractor}, until it can produce the {@link
* SeekMap} and all of the tracks have been identified, or until the extractor encounters EOF.
*
* @param extractor The {@link Extractor} to extractor from input.
* @param output The {@link FakeTrackOutput} to store the extracted {@link SeekMap} and track.
* @param dataSource The {@link DataSource} that will be used to read from the input.
* @param uri The Uri of the input.
* @return The extracted {@link SeekMap}.
* @throws IOException If an error occurred reading from the input, or if the extractor finishes
* reading from input without extracting any {@link SeekMap}.
*/
public static SeekMap extractSeekMap(
Extractor extractor, FakeExtractorOutput output, DataSource dataSource, Uri uri)
throws IOException {
ExtractorInput input = getExtractorInputFromPosition(dataSource, /* position= */ 0, uri);
extractor.init(output);
PositionHolder positionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (true) {
try {
// Keep reading until we can get the seek map
while (readResult == Extractor.RESULT_CONTINUE
&& (output.seekMap == null || !output.tracksEnded)) {
readResult = extractor.read(input, positionHolder);
}
} finally {
Util.closeQuietly(dataSource);
}
if (readResult == Extractor.RESULT_SEEK) {
input = getExtractorInputFromPosition(dataSource, positionHolder.position, uri);
readResult = Extractor.RESULT_CONTINUE;
} else if (readResult == Extractor.RESULT_END_OF_INPUT) {
throw new IOException("EOF encountered without seekmap");
}
if (output.seekMap != null) {
return output.seekMap;
}
}
}
/**
* Extracts all samples from the given file into a {@link FakeTrackOutput}.
*
* @param extractor The {@link Extractor} to extractor from input.
* @param context A {@link Context}.
* @param fileName The name of the input file.
* @return The {@link FakeTrackOutput} containing the extracted samples.
* @throws IOException If an error occurred reading from the input, or if the extractor finishes
* reading from input without extracting any {@link SeekMap}.
*/
public static FakeExtractorOutput extractAllSamplesFromFile(
Extractor extractor, Context context, String fileName) throws IOException {
byte[] data = TestUtil.getByteArray(context, fileName);
FakeExtractorOutput expectedOutput = new FakeExtractorOutput();
extractor.init(expectedOutput);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
PositionHolder positionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
while (readResult == Extractor.RESULT_CONTINUE) {
readResult = extractor.read(input, positionHolder);
}
if (readResult == Extractor.RESULT_SEEK) {
input.setPosition((int) positionHolder.position);
readResult = Extractor.RESULT_CONTINUE;
}
}
return expectedOutput;
}
/**
* Seeks to the given seek time of the stream from the given input, and keeps reading from the
* input until we can extract at least one sample following the seek position, or until
* end-of-input is reached.
*
* @param extractor The {@link Extractor} to extract from input.
* @param seekMap The {@link SeekMap} of the stream from the given input.
* @param seekTimeUs The seek time, in micro-seconds.
* @param trackOutput The {@link FakeTrackOutput} to store the extracted samples.
* @param dataSource The {@link DataSource} that will be used to read from the input.
* @param uri The Uri of the input.
* @return The index of the first extracted sample written to the given {@code trackOutput} after
* the seek is completed, or {@link C#INDEX_UNSET} if the seek is completed without any
* extracted sample.
*/
public static int seekToTimeUs(
Extractor extractor,
SeekMap seekMap,
long seekTimeUs,
DataSource dataSource,
FakeTrackOutput trackOutput,
Uri uri)
throws IOException {
int numSampleBeforeSeek = trackOutput.getSampleCount();
SeekMap.SeekPoints seekPoints = seekMap.getSeekPoints(seekTimeUs);
long initialSeekLoadPosition = seekPoints.first.position;
extractor.seek(initialSeekLoadPosition, seekTimeUs);
PositionHolder positionHolder = new PositionHolder();
positionHolder.position = C.POSITION_UNSET;
ExtractorInput extractorInput =
TestUtil.getExtractorInputFromPosition(dataSource, initialSeekLoadPosition, uri);
int extractorReadResult = Extractor.RESULT_CONTINUE;
while (true) {
try {
// Keep reading until we can read at least one sample after seek
while (extractorReadResult == Extractor.RESULT_CONTINUE
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
extractorReadResult = extractor.read(extractorInput, positionHolder);
}
} finally {
Util.closeQuietly(dataSource);
}
if (extractorReadResult == Extractor.RESULT_SEEK) {
extractorInput =
TestUtil.getExtractorInputFromPosition(dataSource, positionHolder.position, uri);
extractorReadResult = Extractor.RESULT_CONTINUE;
} else if (extractorReadResult == Extractor.RESULT_END_OF_INPUT
&& trackOutput.getSampleCount() == numSampleBeforeSeek) {
return C.INDEX_UNSET;
} else if (trackOutput.getSampleCount() > numSampleBeforeSeek) {
// First index after seek = num sample before seek.
return numSampleBeforeSeek;
}
}
}
/** Returns an {@link ExtractorInput} to read from the given input at given position. */
public static ExtractorInput getExtractorInputFromPosition(
DataSource dataSource, long position, Uri uri) throws IOException {
DataSpec dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET);
long length = dataSource.open(dataSpec);
if (length != C.LENGTH_UNSET) {
length += position;
}
return new DefaultExtractorInput(dataSource, position, length);
}
/**
* Create a new {@link MetadataInputBuffer} and copy {@code data} into the backing {@link
* ByteBuffer}.
*/
public static MetadataInputBuffer createMetadataInputBuffer(byte[] data) {
MetadataInputBuffer buffer = new MetadataInputBuffer();
buffer.data = ByteBuffer.allocate(data.length).put(data);
buffer.data.flip();
return buffer;
}
}