diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTestResult.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTestResult.java index 8365f1d39e..e4ffd81846 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTestResult.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformationTestResult.java @@ -15,31 +15,117 @@ */ package com.google.android.exoplayer2.transformer; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; + /** 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; + /** A builder for {@link TransformationTestResult}. */ + public static class Builder { + private final TransformationResult transformationResult; + + @Nullable private String filePath; + @Nullable private Exception analysisException; + + private long elapsedTimeMs; + private double ssim; + + /** Creates a new {@link Builder}. */ + public Builder(TransformationResult transformationResult) { + this.transformationResult = transformationResult; + this.elapsedTimeMs = C.TIME_UNSET; + this.ssim = SSIM_UNSET; + } + + /** + * Sets the file path of the output file. + * + *
{@code null} represents an unset or unknown value. + * + * @param filePath The path. + * @return This {@link Builder}. + */ + public Builder setFilePath(@Nullable String filePath) { + this.filePath = filePath; + return this; + } + + /** + * Sets the amount of time taken to perform the transformation in milliseconds. {@link + * C#TIME_UNSET} if unset. + * + *
{@link C#TIME_UNSET} represents an unset or unknown value. + * + * @param elapsedTimeMs The time, in ms. + * @return This {@link Builder}. + */ + public Builder setElapsedTimeMs(long elapsedTimeMs) { + this.elapsedTimeMs = elapsedTimeMs; + return this; + } + + /** + * Sets the SSIM of the output file, compared to input file. + * + *
{@link #SSIM_UNSET} represents an unset or unknown value. + * + * @param ssim The structural similarity index. + * @return This {@link Builder}. + */ + public Builder setSsim(double ssim) { + this.ssim = ssim; + return this; + } + + /** + * Sets an {@link Exception} that occurred during post-transformation analysis. + * + *
{@code null} represents an unset or unknown value. + * + * @param analysisException The {@link Exception} thrown during analysis. + * @return This {@link Builder}. + */ + public Builder setAnalysisException(@Nullable Exception analysisException) { + this.analysisException = analysisException; + return this; + } + + /** Builds the {@link TransformationTestResult} instance. */ + public TransformationTestResult build() { + return new TransformationTestResult( + transformationResult, filePath, elapsedTimeMs, ssim, analysisException); + } + } + public final TransformationResult transformationResult; - public final String filePath; - /** The amount of time taken to perform the transformation in milliseconds. */ - public final long transformationDurationMs; + + @Nullable public final String filePath; + /** + * The amount of time taken to perform the transformation in milliseconds. {@link C#TIME_UNSET} if + * unset. + */ + public final long elapsedTimeMs; /** The SSIM score of the transformation, {@link #SSIM_UNSET} if unavailable. */ public final double ssim; + /** + * The {@link Exception} that was thrown during post-tranformation analysis, or {@code null} if + * nothing was thrown. + */ + @Nullable public final Exception analysisException; - public TransformationTestResult( - TransformationResult transformationResult, String filePath, long transformationDurationMs) { - this(transformationResult, filePath, transformationDurationMs, /* ssim= */ SSIM_UNSET); - } - - public TransformationTestResult( + private TransformationTestResult( TransformationResult transformationResult, - String filePath, - long transformationDurationMs, - double ssim) { + @Nullable String filePath, + long elapsedTimeMs, + double ssim, + @Nullable Exception analysisException) { this.transformationResult = transformationResult; this.filePath = filePath; - this.transformationDurationMs = transformationDurationMs; + this.elapsedTimeMs = elapsedTimeMs; this.ssim = ssim; + this.analysisException = analysisException; } } diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerAndroidTestRunner.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerAndroidTestRunner.java index 112307ba0a..1eabc08721 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerAndroidTestRunner.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerAndroidTestRunner.java @@ -39,6 +39,7 @@ import org.json.JSONObject; /** An android instrumentation test runner for {@link Transformer}. */ public class TransformerAndroidTestRunner { + private static final String TAG_PREFIX = "TransformerAndroidTest_"; /** The default transformation timeout value. */ public static final int DEFAULT_TIMEOUT_SECONDS = 120; @@ -49,6 +50,7 @@ public class TransformerAndroidTestRunner { private final Transformer transformer; private boolean calculateSsim; private int timeoutSeconds; + private boolean suppressAnalysisExceptions; /** * Creates a {@link Builder}. @@ -93,9 +95,29 @@ public class TransformerAndroidTestRunner { return this; } + /** + * Sets whether the runner should suppress any {@link Exception} that occurs as a result of + * post-transformation analysis, such as SSIM calculation. + * + *
Regardless of this value, analysis exceptions are attached to the analysis file. + * + *
It's recommended to add a comment explaining why this suppression is needed, ideally with + * a bug number. + * + *
The default value is {@code false}. + * + * @param suppressAnalysisExceptions Whether to suppress analysis exceptions. + * @return This {@link Builder}. + */ + public Builder setSuppressAnalysisExceptions(boolean suppressAnalysisExceptions) { + this.suppressAnalysisExceptions = suppressAnalysisExceptions; + return this; + } + /** Builds the {@link TransformerAndroidTestRunner}. */ public TransformerAndroidTestRunner build() { - return new TransformerAndroidTestRunner(context, transformer, timeoutSeconds, calculateSsim); + return new TransformerAndroidTestRunner( + context, transformer, timeoutSeconds, calculateSsim, suppressAnalysisExceptions); } } @@ -103,13 +125,19 @@ public class TransformerAndroidTestRunner { private final Transformer transformer; private final int timeoutSeconds; private final boolean calculateSsim; + private final boolean suppressAnalysisExceptions; private TransformerAndroidTestRunner( - Context context, Transformer transformer, int timeoutSeconds, boolean calculateSsim) { + Context context, + Transformer transformer, + int timeoutSeconds, + boolean calculateSsim, + boolean suppressAnalysisExceptions) { this.context = context; this.transformer = transformer; this.timeoutSeconds = timeoutSeconds; this.calculateSsim = calculateSsim; + this.suppressAnalysisExceptions = suppressAnalysisExceptions; } /** @@ -126,6 +154,9 @@ public class TransformerAndroidTestRunner { try { TransformationTestResult transformationTestResult = runInternal(testId, uriString); resultJson.put("transformationResult", getTestResultJson(transformationTestResult)); + if (!suppressAnalysisExceptions && transformationTestResult.analysisException != null) { + throw transformationTestResult.analysisException; + } return transformationTestResult; } catch (Exception e) { resultJson.put("exception", getExceptionJson(e)); @@ -147,11 +178,10 @@ public class TransformerAndroidTestRunner { * 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. + * @throws IllegalStateException If an unexpected exception occurs when starting a transformation. */ - private TransformationTestResult runInternal(String testId, String uriString) throws Exception { + private TransformationTestResult runInternal(String testId, String uriString) + throws InterruptedException, IOException, TimeoutException, TransformationException { AtomicReference<@NullableType TransformationException> transformationExceptionReference = new AtomicReference<>(); AtomicReference<@NullableType Exception> unexpectedExceptionReference = new AtomicReference<>(); @@ -201,11 +231,12 @@ public class TransformerAndroidTestRunner { if (!countDownLatch.await(timeoutSeconds, SECONDS)) { throw new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds."); } - long transformationDurationMs = SystemClock.DEFAULT.elapsedRealtime() - startTimeMs; + long elapsedTimeMs = SystemClock.DEFAULT.elapsedRealtime() - startTimeMs; @Nullable Exception unexpectedException = unexpectedExceptionReference.get(); if (unexpectedException != null) { - throw unexpectedException; + throw new IllegalStateException( + "Unexpected exception starting the transformer.", unexpectedException); } @Nullable @@ -222,16 +253,31 @@ public class TransformerAndroidTestRunner { .setFileSizeBytes(outputVideoFile.length()) .build(); - if (!calculateSsim) { - return new TransformationTestResult( - transformationResult, outputVideoFile.getPath(), transformationDurationMs); + TransformationTestResult.Builder resultBuilder = + new TransformationTestResult.Builder(transformationResult) + .setFilePath(outputVideoFile.getPath()) + .setElapsedTimeMs(elapsedTimeMs); + + try { + if (calculateSsim) { + double ssim = + SsimHelper.calculate( + context, /* expectedVideoPath= */ uriString, outputVideoFile.getPath()); + resultBuilder.setSsim(ssim); + } + } catch (InterruptedException interruptedException) { + // InterruptedException is a special unexpected case because it is not related to Ssim + // calculation, so it should be thrown, rather than processed as part of the + // TransformationTestResult. + throw interruptedException; + } catch (Exception analysisException) { + // Catch all (checked and unchecked) exceptions throw by the SsimHelper and process them as + // part of the TransformationTestResult. + resultBuilder.setAnalysisException(analysisException); + Log.e(TAG_PREFIX + testId, "SSIM calculation failed.", analysisException); } - double ssim = - SsimHelper.calculate( - context, /* expectedVideoPath= */ uriString, outputVideoFile.getPath()); - return new TransformationTestResult( - transformationResult, outputVideoFile.getPath(), transformationDurationMs, ssim); + return resultBuilder.build(); } private static void writeTestSummaryToFile(Context context, String testId, JSONObject resultJson) @@ -241,7 +287,7 @@ public class TransformerAndroidTestRunner { 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); + Log.i(TAG_PREFIX + testId, analysisContents); File analysisFile = AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-result.txt"); @@ -272,10 +318,16 @@ public class TransformerAndroidTestRunner { if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { transformationResultJson.put("averageVideoBitrate", transformationResult.averageVideoBitrate); } + if (testResult.elapsedTimeMs != C.TIME_UNSET) { + transformationResultJson.put("elapsedTimeMs", testResult.elapsedTimeMs); + } if (testResult.ssim != TransformationTestResult.SSIM_UNSET) { transformationResultJson.put("ssim", testResult.ssim); } - transformationResultJson.put("transformationDurationMs", testResult.transformationDurationMs); + if (testResult.analysisException != null) { + transformationResultJson.put( + "analysisException", getExceptionJson(testResult.analysisException)); + } return transformationResultJson; } diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/TransformationTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/TransformationTest.java index ec9688fd38..b4b59d6436 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/TransformationTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/TransformationTest.java @@ -47,8 +47,10 @@ public class TransformationTest { Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = new Transformer.Builder(context).build(); - // TODO(b/223381524): Enable Ssim calculation after fixing queueInputBuffer exception. + // TODO(b/223381524): Remove analysis failure suppression after ssim calculation doesn't fail. new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .setSuppressAnalysisExceptions(true) .build() .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); } @@ -87,8 +89,10 @@ public class TransformationTest { } }) .build(); - // TODO(b/223381524): Enable Ssim calculation after fixing queueInputBuffer exception. + // TODO(b/223381524): Remove analysis failure suppression after ssim calculation doesn't fail. new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .setSuppressAnalysisExceptions(true) .build() .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); } @@ -97,10 +101,12 @@ public class TransformationTest { public void transform4K60() throws Exception { final String testId = TAG + "_transform4K60"; - // TODO(b/223381524): Enable Ssim calculation after fixing queueInputBuffer exception. Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = new Transformer.Builder(context).build(); + // TODO(b/223381524): Remove analysis failure suppression after ssim calculation doesn't fail. new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .setSuppressAnalysisExceptions(true) .build() .run(testId, MP4_REMOTE_4K60_PORTRAIT_URI_STRING); } @@ -109,10 +115,12 @@ public class TransformationTest { public void transformNoAudio() throws Exception { final String testId = TAG + "_transformNoAudio"; - // TODO(b/223381524): Enable Ssim calculation after fixing queueInputBuffer exception. Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = new Transformer.Builder(context).setRemoveAudio(true).build(); + // TODO(b/223381524): Remove analysis failure suppression after ssim calculation doesn't fail. new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .setSuppressAnalysisExceptions(true) .build() .run(testId, MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING); } @@ -144,8 +152,10 @@ public class TransformationTest { .setTransformationRequest( new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) .build(); - // TODO(b/223381524): Enable Ssim calculation after fixing queueInputBuffer exception. + // TODO(b/223381524): Remove analysis failure suppression after ssim calculation doesn't fail. new TransformerAndroidTestRunner.Builder(context, transformer) + .setCalculateSsim(true) + .setSuppressAnalysisExceptions(true) .build() .run(testId, MP4_ASSET_SEF_URI_STRING); }