Log ExportResult information when running transformer demo app

Log elapsed time, in addition to displaying it on screen.
Reuse logging logic between tests and demo.

PiperOrigin-RevId: 597186692
This commit is contained in:
Googler 2024-01-10 02:21:55 -08:00 committed by Copybara-Service
parent 143d782b1c
commit 460501fcd1
6 changed files with 261 additions and 114 deletions

View File

@ -58,6 +58,7 @@ import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.datasource.DataSourceBitmapLoader;
import androidx.media3.effect.BitmapOverlay; import androidx.media3.effect.BitmapOverlay;
import androidx.media3.effect.Contrast; import androidx.media3.effect.Contrast;
@ -88,6 +89,7 @@ import androidx.media3.transformer.Effects;
import androidx.media3.transformer.ExportException; import androidx.media3.transformer.ExportException;
import androidx.media3.transformer.ExportResult; import androidx.media3.transformer.ExportResult;
import androidx.media3.transformer.InAppMuxer; import androidx.media3.transformer.InAppMuxer;
import androidx.media3.transformer.JsonUtil;
import androidx.media3.transformer.Muxer; import androidx.media3.transformer.Muxer;
import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.Transformer; import androidx.media3.transformer.Transformer;
@ -110,6 +112,8 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
import org.json.JSONException;
import org.json.JSONObject;
/** An {@link Activity} that exports and plays media using {@link Transformer}. */ /** An {@link Activity} that exports and plays media using {@link Transformer}. */
public final class TransformerActivity extends AppCompatActivity { public final class TransformerActivity extends AppCompatActivity {
@ -347,7 +351,7 @@ public final class TransformerActivity extends AppCompatActivity {
new Transformer.Listener() { new Transformer.Listener() {
@Override @Override
public void onCompleted(Composition composition, ExportResult exportResult) { public void onCompleted(Composition composition, ExportResult exportResult) {
TransformerActivity.this.onCompleted(inputUri, filePath); TransformerActivity.this.onCompleted(inputUri, filePath, exportResult);
} }
@Override @Override
@ -705,13 +709,11 @@ public final class TransformerActivity extends AppCompatActivity {
"debugFrame", "debugFrame",
"exportStopwatch", "exportStopwatch",
}) })
private void onCompleted(Uri inputUri, String filePath) { private void onCompleted(Uri inputUri, String filePath, ExportResult exportResult) {
exportStopwatch.stop(); exportStopwatch.stop();
long elapsedTimeMs = exportStopwatch.elapsed(TimeUnit.MILLISECONDS);
informationTextView.setText( informationTextView.setText(
getString( getString(R.string.export_completed, elapsedTimeMs / 1000.f, filePath));
R.string.export_completed,
exportStopwatch.elapsed(TimeUnit.MILLISECONDS) / 1000.f,
filePath));
progressViewGroup.setVisibility(View.GONE); progressViewGroup.setVisibility(View.GONE);
debugFrame.removeAllViews(); debugFrame.removeAllViews();
inputCardView.setVisibility(View.VISIBLE); inputCardView.setVisibility(View.VISIBLE);
@ -729,6 +731,17 @@ public final class TransformerActivity extends AppCompatActivity {
} }
playMediaItems(MediaItem.fromUri(inputUri), MediaItem.fromUri("file://" + filePath)); playMediaItems(MediaItem.fromUri(inputUri), MediaItem.fromUri("file://" + filePath));
Log.d(TAG, "Output file path: file://" + filePath); Log.d(TAG, "Output file path: file://" + filePath);
try {
JSONObject resultJson =
JsonUtil.exportResultAsJsonObject(exportResult)
.put("elapsedTimeMs", elapsedTimeMs)
.put("device", JsonUtil.getDeviceDetailsAsJsonObject());
for (String line : Util.split(resultJson.toString(2), "\n")) {
Log.d(TAG, line);
}
} catch (JSONException e) {
Log.d(TAG, "Unable to convert exportResult to JSON", e);
}
} }
@RequiresNonNull({ @RequiresNonNull({

View File

@ -30,14 +30,12 @@ import android.media.Image;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.opengl.EGLContext; import android.opengl.EGLContext;
import android.opengl.EGLDisplay; import android.opengl.EGLDisplay;
import android.os.Build;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.ColorInfo; import androidx.media3.common.ColorInfo;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.GlObjectsProvider; import androidx.media3.common.GlObjectsProvider;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
@ -53,7 +51,6 @@ import com.google.common.collect.ImmutableList;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -667,68 +664,6 @@ public final class AndroidTestUtil {
} }
} }
/**
* Returns a {@link JSONObject} containing device specific details from {@link Build}, including
* manufacturer, model, SDK version and build fingerprint.
*/
public static JSONObject getDeviceDetailsAsJsonObject() throws JSONException {
return new JSONObject()
.put("manufacturer", Build.MANUFACTURER)
.put("model", Build.MODEL)
.put("sdkVersion", Build.VERSION.SDK_INT)
.put("fingerprint", Build.FINGERPRINT);
}
/**
* Creates a {@link JSONArray} from {@link ExportResult.ProcessedInput processed inputs}.
*
* @param processedInputs The list of {@link ExportResult.ProcessedInput} instances.
* @return A {@link JSONArray} containing {@link JSONObject} instances representing the {@link
* ExportResult.ProcessedInput} instances.
*/
public static JSONArray processedInputsAsJsonArray(
ImmutableList<ExportResult.ProcessedInput> processedInputs) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < processedInputs.size(); i++) {
ExportResult.ProcessedInput processedInput = processedInputs.get(i);
JSONObject jsonObject = new JSONObject();
@Nullable
MediaItem.LocalConfiguration localConfiguration = processedInput.mediaItem.localConfiguration;
if (localConfiguration != null) {
jsonObject.put("mediaItemUri", localConfiguration.uri);
}
jsonObject.putOpt("audioDecoderName", processedInput.audioDecoderName);
jsonObject.putOpt("videoDecoderName", processedInput.videoDecoderName);
jsonArray.put(jsonObject);
}
return jsonArray;
}
/**
* Creates a {@link JSONObject} from the {@link Exception}.
*
* <p>If the exception is an {@link ExportException}, {@code errorCode} is included.
*
* @param exception The {@link Exception}.
* @return The {@link JSONObject} containing the exception details, or {@code null} if the
* exception was {@code null}.
*/
@Nullable
public static JSONObject exceptionAsJsonObject(@Nullable Exception exception)
throws JSONException {
if (exception == null) {
return null;
}
JSONObject exceptionJson = new JSONObject();
exceptionJson.put("message", exception.getMessage());
exceptionJson.put("type", exception.getClass());
if (exception instanceof ExportException) {
exceptionJson.put("errorCode", ((ExportException) exception).errorCode);
}
exceptionJson.put("stackTrace", Log.getThrowableString(exception));
return exceptionJson;
}
/** /**
* Writes the summary of a test run to the application cache file. * Writes the summary of a test run to the application cache file.
* *
@ -740,7 +675,7 @@ public final class AndroidTestUtil {
*/ */
public static void writeTestSummaryToFile(Context context, String testId, JSONObject testJson) public static void writeTestSummaryToFile(Context context, String testId, JSONObject testJson)
throws IOException, JSONException { throws IOException, JSONException {
testJson.put("testId", testId).put("device", getDeviceDetailsAsJsonObject()); testJson.put("testId", testId).put("device", JsonUtil.getDeviceDetailsAsJsonObject());
String analysisContents = testJson.toString(/* indentSpaces= */ 2); String analysisContents = testJson.toString(/* indentSpaces= */ 2);

View File

@ -15,8 +15,7 @@
*/ */
package androidx.media3.transformer; package androidx.media3.transformer;
import static androidx.media3.transformer.AndroidTestUtil.exceptionAsJsonObject; import static androidx.media3.transformer.JsonUtil.exceptionAsJsonObject;
import static androidx.media3.transformer.AndroidTestUtil.processedInputsAsJsonArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -161,56 +160,21 @@ public class ExportTestResult {
/** Returns a {@link JSONObject} representing all the values in {@code this}. */ /** Returns a {@link JSONObject} representing all the values in {@code this}. */
public JSONObject asJsonObject() throws JSONException { public JSONObject asJsonObject() throws JSONException {
JSONObject jsonObject = JSONObject jsonObject =
new JSONObject() JsonUtil.exportResultAsJsonObject(exportResult)
.putOpt("audioEncoderName", exportResult.audioEncoderName)
.putOpt( .putOpt(
"fallbackDetails", fallbackDetails != null ? fallbackDetails.asJsonObject() : null) "fallbackDetails", fallbackDetails != null ? fallbackDetails.asJsonObject() : null)
.putOpt("filePath", filePath) .putOpt("filePath", filePath)
.putOpt("colorInfo", exportResult.colorInfo)
.putOpt("videoEncoderName", exportResult.videoEncoderName)
.putOpt("testException", exceptionAsJsonObject(exportResult.exportException))
.putOpt("analysisException", exceptionAsJsonObject(analysisException)); .putOpt("analysisException", exceptionAsJsonObject(analysisException));
if (!exportResult.processedInputs.isEmpty()) {
jsonObject.put("processedInputs", processedInputsAsJsonArray(exportResult.processedInputs));
}
if (exportResult.averageAudioBitrate != C.RATE_UNSET_INT) {
jsonObject.put("averageAudioBitrate", exportResult.averageAudioBitrate);
}
if (exportResult.averageVideoBitrate != C.RATE_UNSET_INT) {
jsonObject.put("averageVideoBitrate", exportResult.averageVideoBitrate);
}
if (exportResult.channelCount != C.LENGTH_UNSET) {
jsonObject.put("channelCount", exportResult.channelCount);
}
if (exportResult.durationMs != C.TIME_UNSET) {
jsonObject.put("durationMs", exportResult.durationMs);
}
if (elapsedTimeMs != C.TIME_UNSET) { if (elapsedTimeMs != C.TIME_UNSET) {
jsonObject.put("elapsedTimeMs", elapsedTimeMs); jsonObject.put("elapsedTimeMs", elapsedTimeMs);
} }
if (exportResult.fileSizeBytes != C.LENGTH_UNSET) {
jsonObject.put("fileSizeBytes", exportResult.fileSizeBytes);
}
if (exportResult.height != C.LENGTH_UNSET) {
jsonObject.put("height", exportResult.height);
}
if (exportResult.sampleRate != C.RATE_UNSET_INT) {
jsonObject.put("sampleRate", exportResult.sampleRate);
}
if (ssim != ExportTestResult.SSIM_UNSET) { if (ssim != ExportTestResult.SSIM_UNSET) {
jsonObject.put("ssim", ssim); jsonObject.put("ssim", ssim);
} }
if (throughputFps != C.RATE_UNSET) { if (throughputFps != C.RATE_UNSET) {
jsonObject.put("throughputFps", throughputFps); jsonObject.put("throughputFps", throughputFps);
} }
if (exportResult.videoFrameCount > 0) {
jsonObject.put("videoFrameCount", exportResult.videoFrameCount);
}
if (exportResult.width != C.LENGTH_UNSET) {
jsonObject.put("width", exportResult.width);
}
return jsonObject; return jsonObject;
} }

View File

@ -221,13 +221,11 @@ public class TransformerAndroidTestRunner {
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
resultJson.put( resultJson.put(
"exportResult", "exportResult", new JSONObject().put("testException", JsonUtil.exceptionAsJsonObject(e)));
new JSONObject().put("testException", AndroidTestUtil.exceptionAsJsonObject(e)));
throw e; throw e;
} catch (IOException | TimeoutException | UnsupportedOperationException e) { } catch (IOException | TimeoutException | UnsupportedOperationException e) {
resultJson.put( resultJson.put(
"exportResult", "exportResult", new JSONObject().put("testException", JsonUtil.exceptionAsJsonObject(e)));
new JSONObject().put("testException", AndroidTestUtil.exceptionAsJsonObject(e)));
throw e; throw e;
} finally { } finally {
AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson); AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson);

View File

@ -0,0 +1,147 @@
/*
* Copyright 2024 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
*
* https://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 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.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/** Utilities for working with JSON */
@UnstableApi
public final class JsonUtil {
private JsonUtil() {}
/**
* Returns a {@link JSONObject} containing device specific details from {@link Build}, including
* manufacturer, model, SDK version and build fingerprint.
*/
public static JSONObject getDeviceDetailsAsJsonObject() throws JSONException {
return new JSONObject()
.put("manufacturer", Build.MANUFACTURER)
.put("model", Build.MODEL)
.put("sdkVersion", Build.VERSION.SDK_INT)
.put("fingerprint", Build.FINGERPRINT);
}
/**
* Creates a {@link JSONArray} from {@link ExportResult.ProcessedInput processed inputs}.
*
* @param processedInputs The list of {@link ExportResult.ProcessedInput} instances.
* @return A {@link JSONArray} containing {@link JSONObject} instances representing the {@link
* ExportResult.ProcessedInput} instances.
* @throws JSONException if this method attempts to create a JSON with null key.
*/
public static JSONArray processedInputsAsJsonArray(
ImmutableList<ExportResult.ProcessedInput> processedInputs) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (ExportResult.ProcessedInput processedInput : processedInputs) {
JSONObject jsonObject = new JSONObject();
@Nullable
MediaItem.LocalConfiguration localConfiguration = processedInput.mediaItem.localConfiguration;
if (localConfiguration != null) {
jsonObject.put("mediaItemUri", localConfiguration.uri);
}
jsonObject.putOpt("audioDecoderName", processedInput.audioDecoderName);
jsonObject.putOpt("videoDecoderName", processedInput.videoDecoderName);
jsonArray.put(jsonObject);
}
return jsonArray;
}
/**
* Creates a {@link JSONObject} from the {@link Exception}.
*
* <p>If the exception is an {@link ExportException}, {@code errorCode} is included.
*
* @param exception The {@link Exception}.
* @return The {@link JSONObject} containing the exception details, or {@code null} if the
* exception was {@code null}.
* @throws JSONException if this method attempts to create a JSON with null key.
*/
@Nullable
public static JSONObject exceptionAsJsonObject(@Nullable Exception exception)
throws JSONException {
if (exception == null) {
return null;
}
JSONObject exceptionJson = new JSONObject();
exceptionJson.put("message", exception.getMessage());
exceptionJson.put("type", exception.getClass());
if (exception instanceof ExportException) {
exceptionJson.put("errorCode", ((ExportException) exception).errorCode);
}
exceptionJson.put("stackTrace", Log.getThrowableString(exception));
return exceptionJson;
}
/**
* Creates a {@link JSONObject} from the {@link ExportResult}.
*
* @param exportResult The {@link ExportResult}.
* @return The {@link JSONObject} describing the {@code exportResult}.
* @throws JSONException if this method attempts to create a JSON with null key.
*/
public static JSONObject exportResultAsJsonObject(ExportResult exportResult)
throws JSONException {
JSONObject jsonObject =
new JSONObject()
.putOpt("audioEncoderName", exportResult.audioEncoderName)
.putOpt("colorInfo", exportResult.colorInfo)
.putOpt("videoEncoderName", exportResult.videoEncoderName)
.putOpt("testException", exceptionAsJsonObject(exportResult.exportException));
if (!exportResult.processedInputs.isEmpty()) {
jsonObject.put("processedInputs", processedInputsAsJsonArray(exportResult.processedInputs));
}
if (exportResult.averageAudioBitrate != C.RATE_UNSET_INT) {
jsonObject.put("averageAudioBitrate", exportResult.averageAudioBitrate);
}
if (exportResult.averageVideoBitrate != C.RATE_UNSET_INT) {
jsonObject.put("averageVideoBitrate", exportResult.averageVideoBitrate);
}
if (exportResult.channelCount != C.LENGTH_UNSET) {
jsonObject.put("channelCount", exportResult.channelCount);
}
if (exportResult.durationMs != C.TIME_UNSET) {
jsonObject.put("durationMs", exportResult.durationMs);
}
if (exportResult.fileSizeBytes != C.LENGTH_UNSET) {
jsonObject.put("fileSizeBytes", exportResult.fileSizeBytes);
}
if (exportResult.height != C.LENGTH_UNSET) {
jsonObject.put("height", exportResult.height);
}
if (exportResult.sampleRate != C.RATE_UNSET_INT) {
jsonObject.put("sampleRate", exportResult.sampleRate);
}
if (exportResult.videoFrameCount > 0) {
jsonObject.put("videoFrameCount", exportResult.videoFrameCount);
}
if (exportResult.width != C.LENGTH_UNSET) {
jsonObject.put("width", exportResult.width);
}
return jsonObject;
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2024 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
*
* https://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 com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link JsonUtil} */
@RunWith(AndroidJUnit4.class)
public class JsonUtilTest {
@Test
public void exceptionAsJsonObjectNull() throws JSONException {
assertThat(JsonUtil.exceptionAsJsonObject(null)).isNull();
}
@Test
public void exceptionAsJsonObject_simpleRuntimeException_doesNotThrow() throws JSONException {
RuntimeException exception = new RuntimeException();
JSONObject jsonObject = JsonUtil.exceptionAsJsonObject(exception);
assertThat(jsonObject.length()).isEqualTo(2);
assertThat(jsonObject.get("type")).isEqualTo(exception.getClass());
// Stack trace should contain the name of this method
assertThat(jsonObject.get("stackTrace").toString())
.contains("exceptionAsJsonObject_simpleRuntimeException_doesNotThrow");
}
@Test
public void exceptionAsJsonObject_runtimeExceptionWithMessage_doesNotThrow()
throws JSONException {
RuntimeException exception = new RuntimeException("JsonUtilTest");
JSONObject jsonObject = JsonUtil.exceptionAsJsonObject(exception);
assertThat(jsonObject.length()).isEqualTo(3);
assertThat(jsonObject.get("type")).isEqualTo(exception.getClass());
assertThat(jsonObject.get("message")).isEqualTo("JsonUtilTest");
// Stack trace should contain the name of this method
assertThat(jsonObject.get("stackTrace").toString())
.contains("exceptionAsJsonObject_runtimeExceptionWithMessage_doesNotThrow");
}
@Test
public void exceptionAsJsonObject_exportException_doesNotThrow() throws JSONException {
RuntimeException innerException = new RuntimeException();
ExportException exception =
ExportException.createForMuxer(innerException, ExportException.ERROR_CODE_MUXING_TIMEOUT);
JSONObject jsonObject = JsonUtil.exceptionAsJsonObject(exception);
assertThat(jsonObject.length()).isEqualTo(4);
assertThat(jsonObject.get("type")).isEqualTo(exception.getClass());
assertThat(jsonObject.get("message")).isEqualTo("Muxer error");
assertThat(jsonObject.get("errorCode")).isEqualTo(ExportException.ERROR_CODE_MUXING_TIMEOUT);
// Stack trace should contain the name of this method
assertThat(jsonObject.get("stackTrace").toString())
.contains("exceptionAsJsonObject_exportException_doesNotThrow");
}
@Test
public void getDeviceDetails_keys() throws JSONException {
JSONObject jsonObject = JsonUtil.getDeviceDetailsAsJsonObject();
assertThat(jsonObject.length()).isEqualTo(4);
assertThat(jsonObject.has("manufacturer")).isTrue();
assertThat(jsonObject.has("model")).isTrue();
assertThat(jsonObject.has("sdkVersion")).isTrue();
assertThat(jsonObject.has("fingerprint")).isTrue();
}
}