Create and return a TransformationResult regardless of success.

The TransformationResult has some useful values that are set in error
cases, such as the codecs used.

PiperOrigin-RevId: 495568259
This commit is contained in:
samrobinson 2022-12-15 13:39:53 +00:00 committed by Ian Baker
parent a09bdfe995
commit 63cc0338e1
8 changed files with 175 additions and 113 deletions

View File

@ -311,13 +311,15 @@ public final class TransformerActivity extends AppCompatActivity {
new Transformer.Listener() {
@Override
public void onTransformationCompleted(
MediaItem mediaItem, TransformationResult transformationResult) {
MediaItem mediaItem, TransformationResult result) {
TransformerActivity.this.onTransformationCompleted(filePath, mediaItem);
}
@Override
public void onTransformationError(
MediaItem mediaItem, TransformationException exception) {
MediaItem mediaItem,
TransformationResult result,
TransformationException exception) {
TransformerActivity.this.onTransformationError(exception);
}
})

View File

@ -17,6 +17,7 @@ package androidx.media3.transformer;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.json.JSONException;
import org.json.JSONObject;
@ -28,20 +29,33 @@ public class TransformationTestResult {
/** A builder for {@link TransformationTestResult}. */
public static class Builder {
private final TransformationResult transformationResult;
@Nullable private TransformationResult transformationResult;
@Nullable private String filePath;
@Nullable private Exception analysisException;
private long elapsedTimeMs;
private double ssim;
@Nullable private Exception testException;
@Nullable private Exception analysisException;
/** Creates a new {@link Builder}. */
public Builder(TransformationResult transformationResult) {
this.transformationResult = transformationResult;
public Builder() {
this.elapsedTimeMs = C.TIME_UNSET;
this.ssim = SSIM_UNSET;
}
/**
* Sets the {@link TransformationResult} of the transformation.
*
* <p>This field must be set.
*
* @param transformationResult The {@link TransformationResult}.
* @return This {@link Builder}
*/
@CanIgnoreReturnValue
public Builder setTransformationResult(TransformationResult transformationResult) {
this.transformationResult = transformationResult;
return this;
}
/**
* Sets the file path of the output file.
*
@ -85,6 +99,20 @@ public class TransformationTestResult {
return this;
}
/**
* Sets an {@link Exception} that occurred during the test.
*
* <p>{@code null} represents an unset or unknown value.
*
* @param testException The {@link Exception} thrown during the test.
* @return This {@link Builder}.
*/
@CanIgnoreReturnValue
public Builder setTestException(@Nullable Exception testException) {
this.testException = testException;
return this;
}
/**
* Sets an {@link Exception} that occurred during post-transformation analysis.
*
@ -102,7 +130,12 @@ public class TransformationTestResult {
/** Builds the {@link TransformationTestResult} instance. */
public TransformationTestResult build() {
return new TransformationTestResult(
transformationResult, filePath, elapsedTimeMs, ssim, analysisException);
Assertions.checkNotNull(transformationResult),
filePath,
elapsedTimeMs,
ssim,
testException,
analysisException);
}
}
@ -120,6 +153,12 @@ public class TransformationTestResult {
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 the test, or {@code null} if nothing was thrown.
*/
@Nullable public final Exception testException;
/**
* The {@link Exception} that was thrown during post-transformation analysis, or {@code null} if
* nothing was thrown.
@ -165,6 +204,9 @@ public class TransformationTestResult {
if (ssim != TransformationTestResult.SSIM_UNSET) {
jsonObject.put("ssim", ssim);
}
if (testException != null) {
jsonObject.put("testException", AndroidTestUtil.exceptionAsJsonObject(testException));
}
if (analysisException != null) {
jsonObject.put("analysisException", AndroidTestUtil.exceptionAsJsonObject(analysisException));
}
@ -176,11 +218,13 @@ public class TransformationTestResult {
@Nullable String filePath,
long elapsedTimeMs,
double ssim,
@Nullable Exception testException,
@Nullable Exception analysisException) {
this.transformationResult = transformationResult;
this.filePath = filePath;
this.elapsedTimeMs = elapsedTimeMs;
this.ssim = ssim;
this.testException = testException;
this.analysisException = analysisException;
this.throughputFps =
elapsedTimeMs != C.TIME_UNSET && transformationResult.videoFrameCount > 0

View File

@ -39,7 +39,6 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
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}. */
@ -153,8 +152,6 @@ public class TransformerAndroidTestRunner {
}
private final Context context;
private final CapturingDecoderFactory decoderFactory;
private final CapturingEncoderFactory encoderFactory;
private final Transformer transformer;
private final int timeoutSeconds;
private final boolean requestCalculateSsim;
@ -169,14 +166,7 @@ public class TransformerAndroidTestRunner {
boolean suppressAnalysisExceptions,
@Nullable Map<String, Object> inputValues) {
this.context = context;
this.decoderFactory = new CapturingDecoderFactory(transformer.decoderFactory);
this.encoderFactory = new CapturingEncoderFactory(transformer.encoderFactory);
this.transformer =
transformer
.buildUpon()
.setDecoderFactory(decoderFactory)
.setEncoderFactory(encoderFactory)
.build();
this.transformer = transformer;
this.timeoutSeconds = timeoutSeconds;
this.requestCalculateSsim = requestCalculateSsim;
this.suppressAnalysisExceptions = suppressAnalysisExceptions;
@ -184,7 +174,7 @@ public class TransformerAndroidTestRunner {
}
/**
* Transforms the {@code uriString}, saving a summary of the transformation to the application
* Transforms the {@link MediaItem}, saving a summary of the transformation to the application
* cache.
*
* @param testId A unique identifier for the transformer test run.
@ -200,35 +190,35 @@ public class TransformerAndroidTestRunner {
try {
TransformationTestResult transformationTestResult = runInternal(testId, mediaItem);
resultJson.put("transformationResult", transformationTestResult.asJsonObject());
if (transformationTestResult.testException != null) {
throw transformationTestResult.testException;
}
if (!suppressAnalysisExceptions && transformationTestResult.analysisException != null) {
throw transformationTestResult.analysisException;
}
return transformationTestResult;
} catch (Exception e) {
resultJson.put("exception", AndroidTestUtil.exceptionAsJsonObject(e));
} catch (UnsupportedOperationException | InterruptedException | IOException e) {
resultJson.put(
"transformationResult",
new JSONObject().put("testException", AndroidTestUtil.exceptionAsJsonObject(e)));
throw e;
} finally {
resultJson.put("codecDetails", getCodecNamesAsJsonObject());
AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson);
}
}
/**
* Transforms the {@code uriString}.
* Transforms the {@link MediaItem}.
*
* @param testId An identifier for the test.
* @param mediaItem The {@link MediaItem} 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 an unexpected exception occurs when starting a transformation.
* @throws IOException If an error occurs opening the output file for writing.
*/
private TransformationTestResult runInternal(String testId, MediaItem mediaItem)
throws InterruptedException, IOException, TimeoutException, TransformationException {
throws InterruptedException, IOException {
if (!mediaItem.clippingConfiguration.equals(MediaItem.ClippingConfiguration.UNSET)
&& requestCalculateSsim) {
throw new UnsupportedOperationException(
@ -266,8 +256,11 @@ public class TransformerAndroidTestRunner {
@Override
public void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {
MediaItem inputMediaItem,
TransformationResult result,
TransformationException exception) {
transformationExceptionReference.set(exception);
transformationResultReference.set(result);
countDownLatch.countDown();
}
@ -302,45 +295,54 @@ public class TransformerAndroidTestRunner {
}
});
if (!countDownLatch.await(timeoutSeconds, SECONDS)) {
throw new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds.");
}
long elapsedTimeMs = SystemClock.DEFAULT.elapsedRealtime() - startTimeMs;
// Block here until timeout reached or latch is counted down.
boolean timeoutReached = !countDownLatch.await(timeoutSeconds, SECONDS);
TransformationTestResult.Builder testResultBuilder =
new TransformationTestResult.Builder()
.setElapsedTimeMs(SystemClock.DEFAULT.elapsedRealtime() - startTimeMs);
@Nullable Exception unexpectedException = unexpectedExceptionReference.get();
if (unexpectedException != null) {
throw new IllegalStateException(
"Unexpected exception starting the transformer.", unexpectedException);
}
@Nullable
TransformationException transformationException = transformationExceptionReference.get();
if (transformationException != null) {
throw transformationException;
@Nullable Exception testException = null;
if (timeoutReached) {
testException =
new TimeoutException("Transformer timed out after " + timeoutSeconds + " seconds.");
} else if (unexpectedException != null) {
testException =
new IllegalStateException(
"Unexpected exception starting the transformer.", unexpectedException);
} else if (transformationException != null) {
testException = 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 (testException != null) {
return testResultBuilder
.setTransformationResult(checkNotNull(transformationResultReference.get()))
.setTestException(testException)
.build();
}
TransformationTestResult.Builder resultBuilder =
new TransformationTestResult.Builder(transformationResult)
.setFilePath(outputVideoFile.getPath())
.setElapsedTimeMs(elapsedTimeMs);
// No exceptions raised, transformation has succeeded.
testResultBuilder
.setTransformationResult(
checkNotNull(transformationResultReference.get())
.buildUpon()
.setFileSizeBytes(outputVideoFile.length())
.build())
.setFilePath(outputVideoFile.getPath());
if (!requestCalculateSsim) {
return resultBuilder.build();
return testResultBuilder.build();
}
if (fallbackResolutionApplied.get()) {
Log.i(
TAG,
testId
+ ": Skipping SSIM calculation because an encoder resolution fallback was applied.");
return resultBuilder.build();
return testResultBuilder.build();
}
try {
double ssim =
@ -348,7 +350,7 @@ public class TransformerAndroidTestRunner {
context,
/* referenceVideoPath= */ checkNotNull(mediaItem.localConfiguration).uri.toString(),
/* distortedVideoPath= */ outputVideoFile.getPath());
resultBuilder.setSsim(ssim);
testResultBuilder.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
@ -366,11 +368,11 @@ public class TransformerAndroidTestRunner {
? (Exception) analysisFailure
: new IllegalStateException(analysisFailure);
resultBuilder.setAnalysisException(analysisException);
testResultBuilder.setAnalysisException(analysisException);
Log.e(TAG, testId + ": SSIM calculation failed.", analysisException);
}
}
return resultBuilder.build();
return testResultBuilder.build();
}
/** Returns whether the context is connected to the network. */
@ -400,21 +402,4 @@ public class TransformerAndroidTestRunner {
}
return false;
}
private JSONObject getCodecNamesAsJsonObject() throws JSONException {
JSONObject detailsJson = new JSONObject();
if (decoderFactory.getAudioDecoderName() != null) {
detailsJson.put("audioDecoderName", decoderFactory.getAudioDecoderName());
}
if (decoderFactory.getVideoDecoderName() != null) {
detailsJson.put("videoDecoderName", decoderFactory.getVideoDecoderName());
}
if (encoderFactory.getAudioEncoderName() != null) {
detailsJson.put("audioEncoderName", encoderFactory.getAudioEncoderName());
}
if (encoderFactory.getVideoEncoderName() != null) {
detailsJson.put("videoEncoderName", encoderFactory.getVideoEncoderName());
}
return detailsJson;
}
}

View File

@ -38,6 +38,7 @@ public final class TransformationResult {
@Nullable private String audioEncoderName;
@Nullable private String videoDecoderName;
@Nullable private String videoEncoderName;
@Nullable private TransformationException transformationException;
public Builder() {
durationMs = C.TIME_UNSET;
@ -53,7 +54,7 @@ public final class TransformationResult {
*/
@CanIgnoreReturnValue
public Builder setDurationMs(long durationMs) {
checkArgument(durationMs > 0 || durationMs == C.TIME_UNSET);
checkArgument(durationMs >= 0 || durationMs == C.TIME_UNSET);
this.durationMs = durationMs;
return this;
}
@ -134,6 +135,14 @@ public final class TransformationResult {
return this;
}
/** Sets the {@link TransformationException} that caused the transformation to fail. */
@CanIgnoreReturnValue
public Builder setTransformationException(
@Nullable TransformationException transformationException) {
this.transformationException = transformationException;
return this;
}
public TransformationResult build() {
return new TransformationResult(
durationMs,
@ -144,7 +153,8 @@ public final class TransformationResult {
audioDecoderName,
audioEncoderName,
videoDecoderName,
videoEncoderName);
videoEncoderName,
transformationException);
}
}
@ -162,7 +172,6 @@ public final class TransformationResult {
public final int averageVideoBitrate;
/** The number of video frames. */
public final int videoFrameCount;
/** The name of the audio decoder used, or {@code null} if none were used. */
@Nullable public final String audioDecoderName;
/** The name of the audio encoder used, or {@code null} if none were used. */
@ -171,6 +180,11 @@ public final class TransformationResult {
@Nullable public final String videoDecoderName;
/** The name of the video encoder used, or {@code null} if none were used. */
@Nullable public final String videoEncoderName;
/**
* The {@link TransformationException} that caused the transformation to fail, or {@code null} if
* the transformation was a success.
*/
@Nullable public final TransformationException transformationException;
private TransformationResult(
long durationMs,
@ -181,7 +195,8 @@ public final class TransformationResult {
@Nullable String audioDecoderName,
@Nullable String audioEncoderName,
@Nullable String videoDecoderName,
@Nullable String videoEncoderName) {
@Nullable String videoEncoderName,
@Nullable TransformationException transformationException) {
this.durationMs = durationMs;
this.fileSizeBytes = fileSizeBytes;
this.averageAudioBitrate = averageAudioBitrate;
@ -191,6 +206,7 @@ public final class TransformationResult {
this.audioEncoderName = audioEncoderName;
this.videoDecoderName = videoDecoderName;
this.videoEncoderName = videoEncoderName;
this.transformationException = transformationException;
}
public Builder buildUpon() {
@ -203,7 +219,8 @@ public final class TransformationResult {
.setAudioDecoderName(audioDecoderName)
.setAudioEncoderName(audioEncoderName)
.setVideoDecoderName(videoDecoderName)
.setVideoEncoderName(videoEncoderName);
.setVideoEncoderName(videoEncoderName)
.setTransformationException(transformationException);
}
@Override
@ -223,7 +240,8 @@ public final class TransformationResult {
&& Objects.equals(audioDecoderName, result.audioDecoderName)
&& Objects.equals(audioEncoderName, result.audioEncoderName)
&& Objects.equals(videoDecoderName, result.videoDecoderName)
&& Objects.equals(videoEncoderName, result.videoEncoderName);
&& Objects.equals(videoEncoderName, result.videoEncoderName)
&& Objects.equals(transformationException, result.transformationException);
}
@Override
@ -237,6 +255,7 @@ public final class TransformationResult {
result = 31 * result + Objects.hashCode(audioEncoderName);
result = 31 * result + Objects.hashCode(videoDecoderName);
result = 31 * result + Objects.hashCode(videoEncoderName);
result = 31 * result + Objects.hashCode(transformationException);
return result;
}
}

View File

@ -535,21 +535,33 @@ public final class Transformer {
}
/**
* @deprecated Use {@link #onTransformationError(MediaItem, TransformationException)}.
* @deprecated Use {@link #onTransformationError(MediaItem, TransformationResult,
* TransformationException)}.
*/
@Deprecated
default void onTransformationError(MediaItem inputMediaItem, Exception exception) {
onTransformationError(inputMediaItem, (TransformationException) exception);
}
/**
* @deprecated Use {@link #onTransformationError(MediaItem, TransformationResult,
* TransformationException)}.
*/
@Deprecated
default void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {
onTransformationError(inputMediaItem, new TransformationResult.Builder().build(), exception);
}
/**
* Called if an exception occurs during the transformation.
*
* @param inputMediaItem The {@link MediaItem} for which the exception occurs.
* @param result The {@link TransformationResult} of the transformation.
* @param exception The {@link TransformationException} describing the exception.
*/
default void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {}
MediaItem inputMediaItem, TransformationResult result, TransformationException exception) {}
/**
* Called when fallback to an alternative {@link TransformationRequest} is necessary to comply
@ -880,13 +892,14 @@ public final class Transformer {
}
@Override
public void onTransformationError(TransformationException exception) {
public void onTransformationError(
TransformationResult result, TransformationException exception) {
handler.post(
() -> {
transformerInternal = null;
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationError(mediaItem, exception));
listener -> listener.onTransformationError(mediaItem, result, exception));
listeners.flushEvents();
});
}

View File

@ -16,7 +16,6 @@
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.transformer.TransformationException.ERROR_CODE_MUXING_FAILED;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION;
import static java.lang.annotation.ElementType.TYPE_USE;
@ -57,9 +56,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public interface Listener {
void onTransformationCompleted(TransformationResult transformationResult);
void onTransformationCompleted(TransformationResult result);
void onTransformationError(TransformationException exception);
void onTransformationError(TransformationResult result, TransformationException exception);
}
/**
@ -300,7 +299,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private void endInternal(
@EndReason int endReason, @Nullable TransformationException transformationException) {
@Nullable TransformationResult transformationResult = null;
TransformationResult.Builder transformationResultBuilder =
new TransformationResult.Builder()
.setAudioDecoderName(decoderFactory.getAudioDecoderName())
.setVideoDecoderName(decoderFactory.getVideoDecoderName())
.setAudioEncoderName(encoderFactory.getAudioEncoderName())
.setVideoEncoderName(encoderFactory.getVideoEncoderName());
boolean forCancellation = endReason == END_REASON_CANCELLED;
@Nullable TransformationException releaseTransformationException = null;
if (!released) {
@ -315,26 +320,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
assetLoader.release();
} finally {
try {
for (int i = 0; i < samplePipelines.size(); i++) {
samplePipelines.get(i).release();
if (endReason == END_REASON_COMPLETED) {
transformationResultBuilder
.setDurationMs(muxerWrapper.getDurationMs())
.setFileSizeBytes(muxerWrapper.getCurrentOutputSizeBytes())
.setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO));
}
// TODO(b/250564186): Create TransformationResult on END_REASON_ERROR as well.
if (endReason == END_REASON_COMPLETED) {
transformationResult =
new TransformationResult.Builder()
.setDurationMs(checkNotNull(muxerWrapper).getDurationMs())
.setAverageAudioBitrate(
muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO))
.setAverageVideoBitrate(
muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO))
.setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO))
.setFileSizeBytes(muxerWrapper.getCurrentOutputSizeBytes())
.setAudioDecoderName(decoderFactory.getAudioDecoderName())
.setAudioEncoderName(encoderFactory.getAudioEncoderName())
.setVideoDecoderName(decoderFactory.getVideoDecoderName())
.setVideoEncoderName(encoderFactory.getVideoEncoderName())
.build();
for (int i = 0; i < samplePipelines.size(); i++) {
samplePipelines.get(i).release();
}
} finally {
muxerWrapper.release(forCancellation);
@ -369,9 +365,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
if (exception != null) {
listener.onTransformationError(exception);
listener.onTransformationError(
transformationResultBuilder.setTransformationException(exception).build(), exception);
} else {
listener.onTransformationCompleted(checkNotNull(transformationResult));
listener.onTransformationCompleted(transformationResultBuilder.build());
}
}

View File

@ -337,9 +337,9 @@ public final class TransformerEndToEndTest {
transformer.startTransformation(mediaItem, outputPath);
TransformationException exception = TransformerTestRunner.runUntilError(transformer);
verify(mockListener1).onTransformationError(mediaItem, exception);
verify(mockListener2).onTransformationError(mediaItem, exception);
verify(mockListener3).onTransformationError(mediaItem, exception);
verify(mockListener1).onTransformationError(eq(mediaItem), any(), eq(exception));
verify(mockListener2).onTransformationError(eq(mediaItem), any(), eq(exception));
verify(mockListener3).onTransformationError(eq(mediaItem), any(), eq(exception));
}
@Test

View File

@ -85,7 +85,9 @@ public final class TransformerTestRunner {
@Override
public void onTransformationError(
MediaItem inputMediaItem, TransformationException exception) {
MediaItem inputMediaItem,
TransformationResult result,
TransformationException exception) {
transformationException.set(exception);
}
});