diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index 1a2d7f2e78..9560be07b7 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -15,27 +15,11 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; -import static java.util.concurrent.TimeUnit.SECONDS; import android.content.Context; -import android.net.Uri; -import android.os.Build; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.MediaItem; -import androidx.media3.common.util.Log; -import androidx.test.platform.app.InstrumentationRegistry; import java.io.File; -import java.io.FileWriter; import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; -import org.checkerframework.checker.nullness.compatqual.NullableType; -import org.json.JSONException; -import org.json.JSONObject; /** Utilities for instrumentation tests. */ public final class AndroidTestUtil { @@ -52,192 +36,13 @@ public final class AndroidTestUtil { public static final String MP4_REMOTE_4K60_PORTRAIT_URI_STRING = "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4"; - - /** - * Transforms the {@code uriString} with the {@link Transformer}, saving a summary of the - * transformation to the application cache. - * - * @param context The {@link Context}. - * @param testId An identifier for the test. - * @param transformer The {@link Transformer} that performs the transformation. - * @param uriString The uri (as a {@link String}) that will be transformed. - * @param timeoutSeconds The transformer timeout. An exception is thrown if this is exceeded. - * @param calculateSsim Whether to include SSIM in the {@link TestTransformationResult}. The - * calculation involves decoding and comparing both the input and the output video. - * Consequently this calculation is not cost-free. Requires the input and output video to be - * the same size. - * @return The {@link TestTransformationResult}. - * @throws Exception The cause of the transformation not completing. - */ - public static TestTransformationResult runTransformer( - Context context, - String testId, - Transformer transformer, - String uriString, - int timeoutSeconds, - boolean calculateSsim) - throws Exception { - JSONObject resultJson = new JSONObject(); - try { - TestTransformationResult testTransformationResult = - runTransformerInternal( - context, testId, transformer, uriString, timeoutSeconds, calculateSsim); - resultJson.put( - "transformationResult", - getTransformationResultJson(testTransformationResult.transformationResult)); - if (testTransformationResult.ssim != TestTransformationResult.SSIM_UNSET) { - resultJson.put("ssim", testTransformationResult.ssim); - } - return testTransformationResult; - } catch (Exception e) { - resultJson.put("exception", getExceptionJson(e)); - throw e; - } finally { - writeTestSummaryToFile(context, testId, resultJson); - } - } - - private static TestTransformationResult runTransformerInternal( - Context context, - String testId, - Transformer transformer, - String uriString, - int timeoutSeconds, - boolean calculateSsim) - throws Exception { - AtomicReference<@NullableType TransformationException> transformationExceptionReference = - new AtomicReference<>(); - AtomicReference<@NullableType Exception> unexpectedExceptionReference = new AtomicReference<>(); - AtomicReference<@NullableType TransformationResult> transformationResultReference = - new AtomicReference<>(); - CountDownLatch countDownLatch = new CountDownLatch(1); - - Transformer testTransformer = - transformer - .buildUpon() - .addListener( - new Transformer.Listener() { - @Override - public void onTransformationCompleted( - MediaItem inputMediaItem, TransformationResult result) { - transformationResultReference.set(result); - countDownLatch.countDown(); - } - - @Override - public void onTransformationError( - MediaItem inputMediaItem, TransformationException exception) { - transformationExceptionReference.set(exception); - countDownLatch.countDown(); - } - }) - .build(); - - Uri uri = Uri.parse(uriString); - File outputVideoFile = createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4"); - InstrumentationRegistry.getInstrumentation() - .runOnMainSync( - () -> { - try { - testTransformer.startTransformation( - MediaItem.fromUri(uri), outputVideoFile.getAbsolutePath()); - // Catch all exceptions to report. Exceptions thrown here and not caught will NOT - // propagate. - } catch (Exception e) { - unexpectedExceptionReference.set(e); - countDownLatch.countDown(); - } - }); - - if (!countDownLatch.await(timeoutSeconds, SECONDS)) { - throw new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds."); - } - - @Nullable Exception unexpectedException = unexpectedExceptionReference.get(); - if (unexpectedException != null) { - throw unexpectedException; - } - - @Nullable - TransformationException transformationException = transformationExceptionReference.get(); - if (transformationException != null) { - throw transformationException; - } - - // If both exceptions are null, the Transformation must have succeeded, and a - // transformationResult will be available. - TransformationResult transformationResult = - checkNotNull(transformationResultReference.get()) - .buildUpon() - .setFileSizeBytes(outputVideoFile.length()) - .build(); - - if (calculateSsim) { - return new TestTransformationResult( - transformationResult, - outputVideoFile.getPath(), - SsimHelper.calculate( - context, /* expectedVideoPath= */ uriString, outputVideoFile.getPath())); - } else { - return new TestTransformationResult(transformationResult, outputVideoFile.getPath()); - } - } - - private static void writeTestSummaryToFile(Context context, String testId, JSONObject resultJson) - throws IOException, JSONException { - resultJson.put("testId", testId).put("device", getDeviceJson()); - - String analysisContents = resultJson.toString(/* indentSpaces= */ 2); - - // Log contents as well as writing to file, for easier visibility on individual device testing. - Log.i("TransformerAndroidTest_" + testId, analysisContents); - - File analysisFile = createExternalCacheFile(context, /* fileName= */ testId + "-result.txt"); - try (FileWriter fileWriter = new FileWriter(analysisFile)) { - fileWriter.write(analysisContents); - } - } - - private static File createExternalCacheFile(Context context, String fileName) throws IOException { + /* package */ static File createExternalCacheFile(Context context, String fileName) + throws IOException { File file = new File(context.getExternalCacheDir(), fileName); checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); return file; } - private static JSONObject getDeviceJson() throws JSONException { - return new JSONObject() - .put("manufacturer", Build.MANUFACTURER) - .put("model", Build.MODEL) - .put("sdkVersion", Build.VERSION.SDK_INT) - .put("fingerprint", Build.FINGERPRINT); - } - - private static JSONObject getTransformationResultJson(TransformationResult transformationResult) - throws JSONException { - JSONObject transformationResultJson = new JSONObject(); - if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { - transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes); - } - if (transformationResult.averageAudioBitrate != C.RATE_UNSET_INT) { - transformationResultJson.put("averageAudioBitrate", transformationResult.averageAudioBitrate); - } - if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { - transformationResultJson.put("averageVideoBitrate", transformationResult.averageVideoBitrate); - } - return transformationResultJson; - } - - private static JSONObject getExceptionJson(Exception exception) throws JSONException { - JSONObject exceptionJson = new JSONObject(); - exceptionJson.put("message", exception.getMessage()); - exceptionJson.put("type", exception.getClass()); - if (exception instanceof TransformationException) { - exceptionJson.put("errorCode", ((TransformationException) exception).errorCode); - } - exceptionJson.put("stackTrace", Log.getThrowableString(exception)); - return exceptionJson; - } - private AndroidTestUtil() {} } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TestTransformationResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java similarity index 84% rename from libraries/transformer/src/androidTest/java/androidx/media3/transformer/TestTransformationResult.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java index 033d2b2974..93d2073d04 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TestTransformationResult.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java @@ -15,8 +15,8 @@ */ package androidx.media3.transformer; -/** A test only class for holding additional details alongside a {@link TransformationResult}. */ -public class TestTransformationResult { +/** A test only class for holding the details of a test transformation. */ +public class TransformationTestResult { /** Represents an unset or unknown SSIM score. */ public static final double SSIM_UNSET = -1.0d; @@ -25,11 +25,11 @@ public class TestTransformationResult { /** The SSIM score of the transformation, {@link #SSIM_UNSET} if unavailable. */ public final double ssim; - public TestTransformationResult(TransformationResult transformationResult, String filePath) { + public TransformationTestResult(TransformationResult transformationResult, String filePath) { this(transformationResult, filePath, /* ssim= */ SSIM_UNSET); } - public TestTransformationResult( + public TransformationTestResult( TransformationResult transformationResult, String filePath, double ssim) { this.transformationResult = transformationResult; this.filePath = filePath; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java new file mode 100644 index 0000000000..5d5cb68b98 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -0,0 +1,286 @@ +/* + * Copyright 2022 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 androidx.media3.transformer; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.json.JSONException; +import org.json.JSONObject; + +/** An android instrumentation test runner for {@link Transformer}. */ +public class TransformerAndroidTestRunner { + + /** The default transformation timeout value. */ + public static final int DEFAULT_TIMEOUT_SECONDS = 120; + + /** A {@link Builder} for {@link TransformerAndroidTestRunner} instances. */ + public static class Builder { + private final Context context; + private final Transformer transformer; + private boolean calculateSsim; + private int timeoutSeconds; + + /** + * Creates a {@link Builder}. + * + * @param context The {@link Context}. + * @param transformer The {@link Transformer} that performs the transformation. + */ + public Builder(Context context, Transformer transformer) { + this.context = context; + this.transformer = transformer; + this.timeoutSeconds = DEFAULT_TIMEOUT_SECONDS; + } + + /** + * Sets the timeout in seconds for a single transformation. An exception is thrown when this is + * exceeded. + * + *
The default value is {@link #DEFAULT_TIMEOUT_SECONDS}. + * + * @param timeoutSeconds The timeout. + * @return This {@link Builder}. + */ + public Builder setTimeoutSeconds(int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + return this; + } + + /** + * Sets whether to calculate the SSIM of the transformation output. + * + *
The calculation involves decoding and comparing both the input and the output video. + * Consequently this calculation is not cost-free. Requires the input and output video to be the + * same size. + * + *
The default value is {@code false}.
+ *
+ * @param calculateSsim Whether to calculate SSIM.
+ * @return This {@link Builder}.
+ */
+ public Builder setCalculateSsim(boolean calculateSsim) {
+ this.calculateSsim = calculateSsim;
+ return this;
+ }
+
+ /** Builds the {@link TransformerAndroidTestRunner}. */
+ public TransformerAndroidTestRunner build() {
+ return new TransformerAndroidTestRunner(context, transformer, timeoutSeconds, calculateSsim);
+ }
+ }
+
+ private final Context context;
+ private final Transformer transformer;
+ private final int timeoutSeconds;
+ private final boolean calculateSsim;
+
+ private TransformerAndroidTestRunner(
+ Context context, Transformer transformer, int timeoutSeconds, boolean calculateSsim) {
+ this.context = context;
+ this.transformer = transformer;
+ this.timeoutSeconds = timeoutSeconds;
+ this.calculateSsim = calculateSsim;
+ }
+
+ /**
+ * Transforms the {@code uriString}, saving a summary of the transformation to the application
+ * cache.
+ *
+ * @param testId An identifier for the test.
+ * @param uriString The uri (as a {@link String}) of the file to transform.
+ * @return The {@link TransformationTestResult}.
+ * @throws Exception The cause of the transformation not completing.
+ */
+ public TransformationTestResult run(String testId, String uriString) throws Exception {
+ JSONObject resultJson = new JSONObject();
+ try {
+ TransformationTestResult transformationTestResult = runInternal(testId, uriString);
+ resultJson.put("transformationResult", getTestResultJson(transformationTestResult));
+ return transformationTestResult;
+ } catch (Exception e) {
+ resultJson.put("exception", getExceptionJson(e));
+ throw e;
+ } finally {
+ writeTestSummaryToFile(context, testId, resultJson);
+ }
+ }
+
+ /**
+ * Transforms the {@code uriString}.
+ *
+ * @param testId An identifier for the test.
+ * @param uriString The uri (as a {@link String}) of the file to transform.
+ * @return The {@link TransformationTestResult}.
+ * @throws IOException If an error occurs opening the output file for writing
+ * @throws TimeoutException If the transformation takes longer than the {@link #timeoutSeconds}.
+ * @throws InterruptedException If the thread is interrupted whilst waiting for transformer to
+ * complete.
+ * @throws TransformationException If an exception occurs as a result of the transformation.
+ * @throws IllegalArgumentException If the path is invalid.
+ * @throws IllegalStateException If this method is called from the wrong thread.
+ * @throws IllegalStateException If a transformation is already in progress.
+ * @throws Exception If the transformation did not complete.
+ */
+ private TransformationTestResult runInternal(String testId, String uriString) throws Exception {
+ AtomicReference<@NullableType TransformationException> transformationExceptionReference =
+ new AtomicReference<>();
+ AtomicReference<@NullableType Exception> unexpectedExceptionReference = new AtomicReference<>();
+ AtomicReference<@NullableType TransformationResult> transformationResultReference =
+ new AtomicReference<>();
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+
+ Transformer testTransformer =
+ transformer
+ .buildUpon()
+ .addListener(
+ new Transformer.Listener() {
+ @Override
+ public void onTransformationCompleted(
+ MediaItem inputMediaItem, TransformationResult result) {
+ transformationResultReference.set(result);
+ countDownLatch.countDown();
+ }
+
+ @Override
+ public void onTransformationError(
+ MediaItem inputMediaItem, TransformationException exception) {
+ transformationExceptionReference.set(exception);
+ countDownLatch.countDown();
+ }
+ })
+ .build();
+
+ Uri uri = Uri.parse(uriString);
+ File outputVideoFile =
+ AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4");
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(
+ () -> {
+ try {
+ testTransformer.startTransformation(
+ MediaItem.fromUri(uri), outputVideoFile.getAbsolutePath());
+ // Catch all exceptions to report. Exceptions thrown here and not caught will NOT
+ // propagate.
+ } catch (Exception e) {
+ unexpectedExceptionReference.set(e);
+ countDownLatch.countDown();
+ }
+ });
+
+ if (!countDownLatch.await(timeoutSeconds, SECONDS)) {
+ throw new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds.");
+ }
+
+ @Nullable Exception unexpectedException = unexpectedExceptionReference.get();
+ if (unexpectedException != null) {
+ throw unexpectedException;
+ }
+
+ @Nullable
+ TransformationException transformationException = transformationExceptionReference.get();
+ if (transformationException != null) {
+ throw transformationException;
+ }
+
+ // If both exceptions are null, the Transformation must have succeeded, and a
+ // transformationResult will be available.
+ TransformationResult transformationResult =
+ checkNotNull(transformationResultReference.get())
+ .buildUpon()
+ .setFileSizeBytes(outputVideoFile.length())
+ .build();
+
+ if (!calculateSsim) {
+ return new TransformationTestResult(transformationResult, outputVideoFile.getPath());
+ }
+
+ double ssim =
+ SsimHelper.calculate(
+ context, /* expectedVideoPath= */ uriString, outputVideoFile.getPath());
+ return new TransformationTestResult(transformationResult, outputVideoFile.getPath(), ssim);
+ }
+
+ private static void writeTestSummaryToFile(Context context, String testId, JSONObject resultJson)
+ throws IOException, JSONException {
+ resultJson.put("testId", testId).put("device", getDeviceJson());
+
+ String analysisContents = resultJson.toString(/* indentSpaces= */ 2);
+
+ // Log contents as well as writing to file, for easier visibility on individual device testing.
+ Log.i("TransformerAndroidTest_" + testId, analysisContents);
+
+ File analysisFile =
+ AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-result.txt");
+ try (FileWriter fileWriter = new FileWriter(analysisFile)) {
+ fileWriter.write(analysisContents);
+ }
+ }
+
+ private static JSONObject getDeviceJson() throws JSONException {
+ return new JSONObject()
+ .put("manufacturer", Build.MANUFACTURER)
+ .put("model", Build.MODEL)
+ .put("sdkVersion", Build.VERSION.SDK_INT)
+ .put("fingerprint", Build.FINGERPRINT);
+ }
+
+ private static JSONObject getTestResultJson(TransformationTestResult testResult)
+ throws JSONException {
+ TransformationResult transformationResult = testResult.transformationResult;
+
+ JSONObject transformationResultJson = new JSONObject();
+ if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) {
+ transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes);
+ }
+ if (transformationResult.averageAudioBitrate != C.RATE_UNSET_INT) {
+ transformationResultJson.put("averageAudioBitrate", transformationResult.averageAudioBitrate);
+ }
+ if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) {
+ transformationResultJson.put("averageVideoBitrate", transformationResult.averageVideoBitrate);
+ }
+ if (testResult.ssim != TransformationTestResult.SSIM_UNSET) {
+ transformationResultJson.put("ssim", testResult.ssim);
+ }
+ return transformationResultJson;
+ }
+
+ private static JSONObject getExceptionJson(Exception exception) throws JSONException {
+ JSONObject exceptionJson = new JSONObject();
+ exceptionJson.put("message", exception.getMessage());
+ exceptionJson.put("type", exception.getClass());
+ if (exception instanceof TransformationException) {
+ exceptionJson.put("errorCode", ((TransformationException) exception).errorCode);
+ }
+ exceptionJson.put("stackTrace", Log.getThrowableString(exception));
+ return exceptionJson;
+ }
+}
diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java
index b60946a9f2..0a4044d08e 100644
--- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java
+++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java
@@ -16,7 +16,6 @@
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
-import static androidx.media3.transformer.AndroidTestUtil.runTransformer;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
@@ -54,13 +53,11 @@ public class TransformerEndToEndTest {
// ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames bear-vp9.webm
int expectedFrameCount = 82;
- runTransformer(
- context,
- /* testId= */ "videoTranscoding_completesWithConsistentFrameCount",
- transformer,
- VP9_VIDEO_URI_STRING,
- /* timeoutSeconds= */ 120,
- /* calculateSsim= */ false);
+ new TransformerAndroidTestRunner.Builder(context, transformer)
+ .build()
+ .run(
+ /* testId= */ "videoTranscoding_completesWithConsistentFrameCount",
+ VP9_VIDEO_URI_STRING);
FrameCountingMuxer frameCountingMuxer =
checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated());
@@ -88,13 +85,9 @@ public class TransformerEndToEndTest {
// ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames sample.mp4
int expectedFrameCount = 30;
- runTransformer(
- context,
- /* testId= */ "videoEditing_completesWithConsistentFrameCount",
- transformer,
- AVC_VIDEO_URI_STRING,
- /* timeoutSeconds= */ 120,
- /* calculateSsim= */ false);
+ new TransformerAndroidTestRunner.Builder(context, transformer)
+ .build()
+ .run(/* testId= */ "videoEditing_completesWithConsistentFrameCount", AVC_VIDEO_URI_STRING);
FrameCountingMuxer frameCountingMuxer =
checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated());
diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java
index 1d56256a50..8245e86c06 100644
--- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java
+++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java
@@ -16,16 +16,16 @@
package androidx.media3.transformer.mh;
import static androidx.media3.common.util.Assertions.checkNotNull;
-import static androidx.media3.transformer.AndroidTestUtil.runTransformer;
import static com.google.common.truth.Truth.assertWithMessage;
import android.content.Context;
import android.graphics.Matrix;
import androidx.media3.common.MimeTypes;
import androidx.media3.transformer.AndroidTestUtil;
-import androidx.media3.transformer.TestTransformationResult;
import androidx.media3.transformer.TransformationRequest;
+import androidx.media3.transformer.TransformationTestResult;
import androidx.media3.transformer.Transformer;
+import androidx.media3.transformer.TransformerAndroidTestRunner;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.HashSet;
@@ -56,14 +56,12 @@ public final class RepeatedTranscodeTransformationTest {
Set