From 460501fcd1c531910ee9f76807e2798577a0ddf9 Mon Sep 17 00:00:00 2001 From: Googler Date: Wed, 10 Jan 2024 02:21:55 -0800 Subject: [PATCH] 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 --- .../demo/transformer/TransformerActivity.java | 25 ++- .../media3/transformer/AndroidTestUtil.java | 67 +------- .../media3/transformer/ExportTestResult.java | 40 +---- .../TransformerAndroidTestRunner.java | 6 +- .../androidx/media3/transformer/JsonUtil.java | 147 ++++++++++++++++++ .../media3/transformer/JsonUtilTest.java | 90 +++++++++++ 6 files changed, 261 insertions(+), 114 deletions(-) create mode 100644 libraries/transformer/src/main/java/androidx/media3/transformer/JsonUtil.java create mode 100644 libraries/transformer/src/test/java/androidx/media3/transformer/JsonUtilTest.java diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index b63b4d4744..58ce6ca4c5 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -58,6 +58,7 @@ import androidx.media3.common.audio.SonicAudioProcessor; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.Clock; import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.effect.BitmapOverlay; import androidx.media3.effect.Contrast; @@ -88,6 +89,7 @@ import androidx.media3.transformer.Effects; import androidx.media3.transformer.ExportException; import androidx.media3.transformer.ExportResult; import androidx.media3.transformer.InAppMuxer; +import androidx.media3.transformer.JsonUtil; import androidx.media3.transformer.Muxer; import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.Transformer; @@ -110,6 +112,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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}. */ public final class TransformerActivity extends AppCompatActivity { @@ -347,7 +351,7 @@ public final class TransformerActivity extends AppCompatActivity { new Transformer.Listener() { @Override public void onCompleted(Composition composition, ExportResult exportResult) { - TransformerActivity.this.onCompleted(inputUri, filePath); + TransformerActivity.this.onCompleted(inputUri, filePath, exportResult); } @Override @@ -705,13 +709,11 @@ public final class TransformerActivity extends AppCompatActivity { "debugFrame", "exportStopwatch", }) - private void onCompleted(Uri inputUri, String filePath) { + private void onCompleted(Uri inputUri, String filePath, ExportResult exportResult) { exportStopwatch.stop(); + long elapsedTimeMs = exportStopwatch.elapsed(TimeUnit.MILLISECONDS); informationTextView.setText( - getString( - R.string.export_completed, - exportStopwatch.elapsed(TimeUnit.MILLISECONDS) / 1000.f, - filePath)); + getString(R.string.export_completed, elapsedTimeMs / 1000.f, filePath)); progressViewGroup.setVisibility(View.GONE); debugFrame.removeAllViews(); inputCardView.setVisibility(View.VISIBLE); @@ -729,6 +731,17 @@ public final class TransformerActivity extends AppCompatActivity { } playMediaItems(MediaItem.fromUri(inputUri), MediaItem.fromUri("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({ 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 2f5d7498d1..5de5d9c7dd 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -30,14 +30,12 @@ import android.media.Image; import android.media.MediaFormat; import android.opengl.EGLContext; import android.opengl.EGLDisplay; -import android.os.Build; import android.util.Pair; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.GlObjectsProvider; -import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.Log; @@ -53,7 +51,6 @@ import com.google.common.collect.ImmutableList; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import org.json.JSONArray; import org.json.JSONException; 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 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}. - * - *

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. * @@ -740,7 +675,7 @@ public final class AndroidTestUtil { */ public static void writeTestSummaryToFile(Context context, String testId, JSONObject testJson) throws IOException, JSONException { - testJson.put("testId", testId).put("device", getDeviceDetailsAsJsonObject()); + testJson.put("testId", testId).put("device", JsonUtil.getDeviceDetailsAsJsonObject()); String analysisContents = testJson.toString(/* indentSpaces= */ 2); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ExportTestResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ExportTestResult.java index e721962ba4..c83a65a610 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ExportTestResult.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/ExportTestResult.java @@ -15,8 +15,7 @@ */ package androidx.media3.transformer; -import static androidx.media3.transformer.AndroidTestUtil.exceptionAsJsonObject; -import static androidx.media3.transformer.AndroidTestUtil.processedInputsAsJsonArray; +import static androidx.media3.transformer.JsonUtil.exceptionAsJsonObject; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -161,56 +160,21 @@ public class ExportTestResult { /** Returns a {@link JSONObject} representing all the values in {@code this}. */ public JSONObject asJsonObject() throws JSONException { JSONObject jsonObject = - new JSONObject() - .putOpt("audioEncoderName", exportResult.audioEncoderName) + JsonUtil.exportResultAsJsonObject(exportResult) .putOpt( "fallbackDetails", fallbackDetails != null ? fallbackDetails.asJsonObject() : null) .putOpt("filePath", filePath) - .putOpt("colorInfo", exportResult.colorInfo) - .putOpt("videoEncoderName", exportResult.videoEncoderName) - .putOpt("testException", exceptionAsJsonObject(exportResult.exportException)) .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) { 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) { jsonObject.put("ssim", ssim); } if (throughputFps != C.RATE_UNSET) { 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; } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java index 30f9fccd5a..80b41ef694 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -221,13 +221,11 @@ public class TransformerAndroidTestRunner { } catch (InterruptedException e) { Thread.currentThread().interrupt(); resultJson.put( - "exportResult", - new JSONObject().put("testException", AndroidTestUtil.exceptionAsJsonObject(e))); + "exportResult", new JSONObject().put("testException", JsonUtil.exceptionAsJsonObject(e))); throw e; } catch (IOException | TimeoutException | UnsupportedOperationException e) { resultJson.put( - "exportResult", - new JSONObject().put("testException", AndroidTestUtil.exceptionAsJsonObject(e))); + "exportResult", new JSONObject().put("testException", JsonUtil.exceptionAsJsonObject(e))); throw e; } finally { AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/JsonUtil.java b/libraries/transformer/src/main/java/androidx/media3/transformer/JsonUtil.java new file mode 100644 index 0000000000..c7e923362e --- /dev/null +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/JsonUtil.java @@ -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 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}. + * + *

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; + } +} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/JsonUtilTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/JsonUtilTest.java new file mode 100644 index 0000000000..d35f24f90e --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/JsonUtilTest.java @@ -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(); + } +}