Merge "Updated mediaediting tests to androidx.media3 version 1.3.0" into main
diff --git a/tests/tests/mediaediting/Android.bp b/tests/tests/mediaediting/Android.bp
index 654fa1a..4f35a8a 100644
--- a/tests/tests/mediaediting/Android.bp
+++ b/tests/tests/mediaediting/Android.bp
@@ -27,6 +27,7 @@
         "androidx.media3.media3-common",
         "androidx.media3.media3-effect",
         "androidx.media3.media3-exoplayer",
+        "androidx.media3.media3-test-utils",
         "androidx.media3.media3-transformer",
         "ctsmediav2common",
     ],
@@ -40,7 +41,6 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-media",
     ],
     host_required: ["cts-dynamic-config"],
     min_sdk_version: "29",
diff --git a/tests/tests/mediaediting/README.md b/tests/tests/mediaediting/README.md
index 1cee0a9..8bd6e2f 100644
--- a/tests/tests/mediaediting/README.md
+++ b/tests/tests/mediaediting/README.md
@@ -22,7 +22,7 @@
 No Change.
 
 ### FileUtil.java
-Commented out the code which asserts if total track count in media is not equal to 2, as this check restricts usage of input clips which are having only one video track which is a valid use case scenario.
+No Change.
 
 ### MssimCalculator.java
 No change.
@@ -31,7 +31,7 @@
 No change.
 
 ### TransformerAndroidTestRunner.java
-Remove usage of NullableType annotation as library is not accessible for it.
+No Change.
 
 ### VideoDecodingWrapper.java
 No change.
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/AndroidTestUtil.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/AndroidTestUtil.java
index aea3a26..6517f2f 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/AndroidTestUtil.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/AndroidTestUtil.java
@@ -21,22 +21,21 @@
 import static androidx.media3.common.MimeTypes.VIDEO_H265;
 import static androidx.media3.common.util.Assertions.checkNotNull;
 import static androidx.media3.common.util.Assertions.checkState;
+import static androidx.media3.common.util.Util.SDK_INT;
+import static org.junit.Assume.assumeFalse;
 
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.media.Image;
 import android.media.MediaFormat;
 import android.opengl.EGLContext;
 import android.opengl.EGLDisplay;
-import android.opengl.GLES20;
-import android.opengl.GLUtils;
-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;
@@ -45,18 +44,20 @@
 import androidx.media3.effect.DefaultGlObjectsProvider;
 import androidx.media3.effect.ScaleAndRotateTransformation;
 import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
+import androidx.media3.test.utils.BitmapPixelTestUtil;
+import androidx.media3.test.utils.VideoDecodingWrapper;
 import androidx.media3.transformer.Codec;
 import androidx.media3.transformer.DefaultEncoderFactory;
 import androidx.media3.transformer.DefaultMuxer;
-import androidx.media3.transformer.EncoderUtil;
 import androidx.media3.transformer.Effects;
+import androidx.media3.transformer.EncoderUtil;
 import androidx.media3.transformer.ExportException;
-import androidx.media3.transformer.ExportResult;
+import androidx.media3.transformer.JsonUtil;
+import com.google.common.base.Ascii;
 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;
 
@@ -64,13 +65,6 @@
 public final class AndroidTestUtil {
   private static final String TAG = "AndroidTestUtil";
 
-  /** A realtime {@linkplain MediaFormat#KEY_PRIORITY encoder priority}. */
-  public static final int MEDIA_CODEC_PRIORITY_REALTIME = 0;
-  /**
-   * A non-realtime (as fast as possible) {@linkplain MediaFormat#KEY_PRIORITY encoder priority}.
-   */
-  public static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 1;
-
   /** An {@link Effects} instance that forces video transcoding. */
   public static final Effects FORCE_TRANSCODE_VIDEO_EFFECTS =
       new Effects(
@@ -81,6 +75,11 @@
   public static final String PNG_ASSET_URI_STRING =
       "asset:///media/bitmap/input_images/media3test.png";
   public static final String JPG_ASSET_URI_STRING = "asset:///media/bitmap/input_images/london.jpg";
+  public static final String JPG_PORTRAIT_ASSET_URI_STRING =
+      "asset:///media/bitmap/input_images/tokyo.jpg";
+
+  public static final String MP4_TRIM_OPTIMIZATION_URI_STRING =
+      "asset:///media/mp4/internal_emulator_transformer_output.mp4";
 
   public static final String MP4_ASSET_URI_STRING = "asset:///media/mp4/sample.mp4";
   public static final Format MP4_ASSET_FORMAT =
@@ -92,6 +91,21 @@
           .setCodecs("avc1.64001F")
           .build();
 
+  // Result of the following command for MP4_ASSET_URI_STRING
+  // ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames sample.mp4
+  public static final int MP4_ASSET_FRAME_COUNT = 30;
+
+  public static final String MP4_PORTRAIT_ASSET_URI_STRING =
+      "asset:///media/mp4/sample_portrait.mp4";
+  public static final Format MP4_PORTRAIT_ASSET_FORMAT =
+      new Format.Builder()
+          .setSampleMimeType(VIDEO_H264)
+          .setWidth(720)
+          .setHeight(1080)
+          .setFrameRate(29.97f)
+          .setCodecs("avc1.64001F")
+          .build();
+
   public static final String MP4_ASSET_AV1_VIDEO_URI_STRING = "asset:///media/mp4/sample_av1.mp4";
   public static final Format MP4_ASSET_AV1_VIDEO_FORMAT =
       new Format.Builder()
@@ -136,6 +150,17 @@
           .setCodecs("avc1.64000D")
           .build();
 
+  public static final String MP4_ASSET_SEF_H265_URI_STRING =
+      "asset:///media/mp4/sample_sef_slow_motion_hevc.mp4";
+  public static final Format MP4_ASSET_SEF_H265_FORMAT =
+      new Format.Builder()
+          .setSampleMimeType(VIDEO_H265)
+          .setWidth(1920)
+          .setHeight(1080)
+          .setFrameRate(30.01679f)
+          .setCodecs("hvc1.1.6.L120.B0")
+          .build();
+
   public static final String MP4_ASSET_BT2020_SDR = "asset:///media/mp4/bt2020-sdr.mp4";
   public static final Format MP4_ASSET_BT2020_SDR_FORMAT =
       new Format.Builder()
@@ -236,9 +261,8 @@
           .setCodecs("avc1.64001F")
           .build();
 
-  public static final String MP4_REMOTE_8K24_URI_STRING =
-      "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4";
-  public static final Format MP4_REMOTE_8K24_FORMAT =
+  public static final String MP4_ASSET_8K24_URI_STRING = "asset:///media/mp4/8k24fps_300ms.mp4";
+  public static final Format MP4_ASSET_8K24_FORMAT =
       new Format.Builder()
           .setSampleMimeType(MimeTypes.VIDEO_H265)
           .setWidth(7680)
@@ -546,20 +570,20 @@
           .setCodecs("hvc1.1.6.L183.B0")
           .build();
 
-  public static final String MP3_ASSET_URI_STRING = "asset:///media/mp3/test.mp3";
+  public static final String MP3_ASSET_URI_STRING = "asset:///media/mp3/test-cbr-info-header.mp3";
 
   /**
    * Creates the GL objects needed to set up a GL environment including an {@link EGLDisplay} and an
    * {@link EGLContext}.
    */
   public static EGLContext createOpenGlObjects() throws GlUtil.GlException {
-    EGLDisplay eglDisplay = GlUtil.createEglDisplay();
-    int[] configAttributes = GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888;
+    EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
     GlObjectsProvider glObjectsProvider =
         new DefaultGlObjectsProvider(/* sharedEglContext= */ null);
     EGLContext eglContext =
-        glObjectsProvider.createEglContext(eglDisplay, /* openGlVersion= */ 2, configAttributes);
-    glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext, eglDisplay, configAttributes);
+        glObjectsProvider.createEglContext(
+            eglDisplay, /* openGlVersion= */ 2, GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888);
+    glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext, eglDisplay);
     return eglContext;
   }
 
@@ -570,13 +594,7 @@
    * <p>Must have a GL context set up.
    */
   public static int generateTextureFromBitmap(Bitmap bitmap) throws GlUtil.GlException {
-    int texId =
-        GlUtil.createTexture(
-            bitmap.getWidth(), bitmap.getHeight(), /* useHighPrecisionColorComponents= */ false);
-    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
-    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0);
-    GlUtil.checkGlError();
-    return texId;
+    return GlUtil.createTexture(bitmap);
   }
 
   /**
@@ -596,6 +614,27 @@
     writeTestSummaryToFile(context, testId, testJson);
   }
 
+  public static ImmutableList<Bitmap> extractBitmapsFromVideo(Context context, String filePath)
+      throws IOException, InterruptedException {
+    // b/298599172 - runUntilComparisonFrameOrEnded fails on this device because reading decoder
+    //  output as a bitmap doesn't work.
+    assumeFalse(Util.SDK_INT == 21 && Ascii.toLowerCase(Util.MODEL).contains("nexus"));
+    ImmutableList.Builder<Bitmap> bitmaps = new ImmutableList.Builder<>();
+    try (VideoDecodingWrapper decodingWrapper =
+        new VideoDecodingWrapper(
+            context, filePath, /* comparisonInterval= */ 1, /* maxImagesAllowed= */ 1)) {
+      while (true) {
+        @Nullable Image image = decodingWrapper.runUntilComparisonFrameOrEnded();
+        if (image == null) {
+          break;
+        }
+        bitmaps.add(BitmapPixelTestUtil.createGrayscaleArgb8888BitmapFromYuv420888Image(image));
+        image.close();
+      }
+    }
+    return bitmaps.build();
+  }
+
   /** A customizable forwarding {@link Codec.EncoderFactory} that forces encoding. */
   public static final class ForceEncodeEncoderFactory implements Codec.EncoderFactory {
 
@@ -636,68 +675,6 @@
   }
 
   /**
-   * 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.
    *
    * <p>The cache filename follows the pattern {@code <testId>-result.txt}.
@@ -708,7 +685,7 @@
    */
   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);
 
@@ -780,6 +757,8 @@
         return MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_FORMAT;
       case MP4_ASSET_SEF_URI_STRING:
         return MP4_ASSET_SEF_FORMAT;
+      case MP4_ASSET_SEF_H265_URI_STRING:
+        return MP4_ASSET_SEF_H265_FORMAT;
       case MP4_ASSET_4K60_PORTRAIT_URI_STRING:
         return MP4_ASSET_4K60_PORTRAIT_FORMAT;
       case MP4_REMOTE_10_SECONDS_URI_STRING:
@@ -850,7 +829,19 @@
       MediaFormatUtil.maybeSetInteger(
           mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first);
     }
-    return EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true) != null;
+    return EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true) != null
+        && !deviceNeedsDisable8kWorkaround(format);
+  }
+
+  private static boolean deviceNeedsDisable8kWorkaround(Format format) {
+    // Fixed on API 31+. See http://b/278234847#comment40 for more information.
+    // Duplicate of DefaultDecoderFactory#deviceNeedsDisable8kWorkaround.
+    return SDK_INT < 31
+        && format.width >= 7680
+        && format.height >= 4320
+        && format.sampleMimeType != null
+        && format.sampleMimeType.equals(MimeTypes.VIDEO_H265)
+        && (Util.MODEL.equals("SM-F711U1") || Util.MODEL.equals("SM-F926U1"));
   }
 
   private static boolean canEncode(Format format) {
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/ExportTestResult.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/ExportTestResult.java
index e5b077d..b575670 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/ExportTestResult.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/ExportTestResult.java
@@ -15,12 +15,12 @@
  */
 package android.media.mediaediting.cts;
 
-import static android.media.mediaediting.cts.AndroidTestUtil.exceptionAsJsonObject;
-import static android.media.mediaediting.cts.AndroidTestUtil.processedInputsAsJsonArray;
+import static androidx.media3.transformer.JsonUtil.exceptionAsJsonObject;
 
 import androidx.annotation.Nullable;
 import androidx.media3.common.C;
 import androidx.media3.transformer.ExportResult;
+import androidx.media3.transformer.JsonUtil;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -128,25 +128,31 @@
 
   /** The {@link ExportResult} of the export. */
   public final ExportResult exportResult;
+
   /** The path to the file created in the export, or {@code null} if unset. */
   @Nullable public final String filePath;
+
   /**
    * The amount of time taken to perform the export in milliseconds, or {@link C#TIME_UNSET} if
    * unset.
    */
   public final long elapsedTimeMs;
+
   /**
    * The average rate (per second) at which frames were processed by the transformer, or {@link
    * C#RATE_UNSET} if unset.
    */
   public final float throughputFps;
+
   /** The SSIM score of the export, or {@link #SSIM_UNSET} if unset. */
   public final double ssim;
+
   /**
    * The {@link FallbackDetails} describing the fallbacks that occurred doing export, or {@code
    * null} if no fallback occurred.
    */
   @Nullable public final FallbackDetails fallbackDetails;
+
   /**
    * The {@link Exception} thrown during post-export analysis, or {@code null} if nothing was
    * thrown.
@@ -156,56 +162,21 @@
   /** 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/tests/tests/mediaediting/src/android/media/mediaediting/cts/FallbackDetails.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/FallbackDetails.java
index d5e3d15..dd5a291 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/FallbackDetails.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/FallbackDetails.java
@@ -17,7 +17,7 @@
 
 import androidx.annotation.Nullable;
 import androidx.media3.common.C;
-import androidx.media3.transformer.TransformationRequest.HdrMode;
+import androidx.media3.transformer.Composition.HdrMode;
 import java.util.Objects;
 import org.json.JSONException;
 import org.json.JSONObject;
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/FileUtil.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/FileUtil.java
index 4882673..a1b561f 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/FileUtil.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/FileUtil.java
@@ -46,13 +46,14 @@
       trackGroupArray =
           MetadataRetriever.retrieveMetadata(context, MediaItem.fromUri("file://" + filePath))
               .get();
-    } catch (ExecutionException | InterruptedException e) {
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new IllegalStateException(e);
+    } catch (ExecutionException e) {
       throw new IllegalStateException(e);
     }
 
-    int trackGroupCount = trackGroupArray.length;
-//    assertThat(trackGroupCount).isEqualTo(2);
-    for (int i = 0; i < trackGroupCount; i++) {
+    for (int i = 0; i < trackGroupArray.length; i++) {
       TrackGroup trackGroup = trackGroupArray.get(i);
       if (trackGroup.type == C.TRACK_TYPE_VIDEO) {
         assertThat(trackGroup.length).isEqualTo(1);
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/SsimHelper.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/SsimHelper.java
index a2d8f48..345dc2b 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/SsimHelper.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/SsimHelper.java
@@ -24,6 +24,8 @@
 import android.media.Image;
 import android.media.MediaCodec;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.media3.common.util.UnstableApi;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
@@ -40,6 +42,8 @@
  * <p>SSIM is traditionally computed with the luminance channel (Y), this class uses the luma
  * channel (Y') because the {@linkplain MediaCodec decoder} decodes to luma.
  */
+@UnstableApi
+@RequiresApi(21)
 public final class SsimHelper {
 
   /** The default comparison interval. */
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformHdrToSdrToneMapTest.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformHdrToSdrToneMapTest.java
index 6b3f04d..bbe7943 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformHdrToSdrToneMapTest.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformHdrToSdrToneMapTest.java
@@ -37,6 +37,7 @@
 import androidx.media3.common.MimeTypes;
 import androidx.media3.transformer.ExportException;
 import androidx.media3.transformer.TransformationRequest;
+import androidx.media3.transformer.Composition;
 import androidx.media3.transformer.Transformer;
 import androidx.test.core.app.ApplicationProvider;
 
@@ -124,7 +125,7 @@
     return new Transformer.Builder(context)
         .setTransformationRequest(
             new TransformationRequest.Builder()
-                .setHdrMode(TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC)
+                .setHdrMode(Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC)
                 .build())
         .addListener(
             new Transformer.Listener() {
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformerAndroidTestRunner.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformerAndroidTestRunner.java
index 6ccda9b..36cf2f0 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformerAndroidTestRunner.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/TransformerAndroidTestRunner.java
@@ -28,20 +28,23 @@
 import androidx.annotation.Nullable;
 import androidx.media3.common.C;
 import androidx.media3.common.MediaItem;
+import androidx.media3.common.util.Clock;
 import androidx.media3.common.util.Log;
+import androidx.media3.common.util.NullableType;
 import androidx.media3.common.util.SystemClock;
 import androidx.media3.common.util.Util;
 import androidx.media3.effect.DebugTraceUtil;
+import androidx.media3.test.utils.SsimHelper;
 import androidx.media3.transformer.Composition;
 import androidx.media3.transformer.EditedMediaItem;
 import androidx.media3.transformer.EditedMediaItemSequence;
 import androidx.media3.transformer.ExportException;
 import androidx.media3.transformer.ExportResult;
+import androidx.media3.transformer.JsonUtil;
 import androidx.media3.transformer.TransformationRequest;
 import androidx.media3.transformer.Transformer;
 import androidx.test.platform.app.InstrumentationRegistry;
 import com.google.common.base.Ascii;
-import com.google.common.collect.ImmutableList;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
 import java.io.File;
 import java.io.IOException;
@@ -50,7 +53,6 @@
 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.JSONObject;
 
 /** An android instrumentation test runner for {@link Transformer}. */
@@ -194,12 +196,28 @@
    * @throws Exception The cause of the export not completing.
    */
   public ExportTestResult run(String testId, Composition composition) throws Exception {
+    return run(testId, composition, /* oldFilePath= */ null);
+  }
+
+  /**
+   * Exports the {@link Composition}, saving a summary of the export to the application cache.
+   * Resumes exporting if the {@code oldFilePath} is specified.
+   *
+   * @param testId A unique identifier for the transformer test run.
+   * @param composition The {@link Composition} to export.
+   * @param oldFilePath The old output file path to resume the export from. Passing {@code null}
+   *     will restart the export from the beginning.
+   * @return The {@link ExportTestResult}.
+   * @throws Exception The cause of the export not completing.
+   */
+  public ExportTestResult run(String testId, Composition composition, @Nullable String oldFilePath)
+      throws Exception {
     JSONObject resultJson = new JSONObject();
     if (inputValues != null) {
       resultJson.put("inputValues", JSONObject.wrap(inputValues));
     }
     try {
-      ExportTestResult exportTestResult = runInternal(testId, composition);
+      ExportTestResult exportTestResult = runInternal(testId, composition, oldFilePath);
       resultJson.put("exportResult", exportTestResult.asJsonObject());
       if (exportTestResult.exportResult.exportException != null) {
         throw exportTestResult.exportResult.exportException;
@@ -208,13 +226,14 @@
         throw exportTestResult.analysisException;
       }
       return exportTestResult;
-    } catch (InterruptedException
-        | IOException
-        | TimeoutException
-        | UnsupportedOperationException e) {
+    } 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", JsonUtil.exceptionAsJsonObject(e)));
       throw e;
     } finally {
       AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson);
@@ -230,9 +249,8 @@
    * @throws Exception The cause of the export not completing.
    */
   public ExportTestResult run(String testId, EditedMediaItem editedMediaItem) throws Exception {
-    EditedMediaItemSequence sequence =
-        new EditedMediaItemSequence(ImmutableList.of(editedMediaItem));
-    Composition composition = new Composition.Builder(ImmutableList.of(sequence)).build();
+    Composition composition =
+        new Composition.Builder(new EditedMediaItemSequence(editedMediaItem)).build();
     return run(testId, composition);
   }
 
@@ -254,6 +272,8 @@
    *
    * @param testId An identifier for the test.
    * @param composition The {@link Composition} to export.
+   * @param oldFilePath The old output file path to resume the export from. Passing {@code null}
+   *     will restart the export from the beginning.
    * @return The {@link ExportTestResult}.
    * @throws IllegalStateException See {@link Transformer#start(Composition, String)}.
    * @throws InterruptedException If the thread is interrupted whilst waiting for transformer to
@@ -262,7 +282,8 @@
    * @throws TimeoutException If the export has not completed after {@linkplain
    *     Builder#setTimeoutSeconds(int) the given timeout}.
    */
-  private ExportTestResult runInternal(String testId, Composition composition)
+  private ExportTestResult runInternal(
+      String testId, Composition composition, @Nullable String oldFilePath)
       throws InterruptedException, IOException, TimeoutException {
     if (requestCalculateSsim) {
       checkArgument(
@@ -294,13 +315,15 @@
       }
     }
 
-    AtomicReference<FallbackDetails> fallbackDetailsReference =
+    AtomicReference<@NullableType FallbackDetails> fallbackDetailsReference =
         new AtomicReference<>();
-    AtomicReference<Exception> unexpectedExceptionReference = new AtomicReference<>();
-    AtomicReference<ExportResult> exportResultReference = new AtomicReference<>();
+    AtomicReference<@NullableType Exception> unexpectedExceptionReference = new AtomicReference<>();
+    AtomicReference<@NullableType ExportResult> exportResultReference = new AtomicReference<>();
     CountDownLatch countDownLatch = new CountDownLatch(1);
     long startTimeMs = SystemClock.DEFAULT.elapsedRealtime();
 
+    DebugTraceUtil.enableTracing = true;
+
     Transformer testTransformer =
         transformer
             .buildUpon()
@@ -344,12 +367,19 @@
             .build();
 
     File outputVideoFile =
-        AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-output.mp4");
+        AndroidTestUtil.createExternalCacheFile(
+            context,
+            /* fileName= */ testId + "-" + Clock.DEFAULT.elapsedRealtime() + "-output.mp4");
     InstrumentationRegistry.getInstrumentation()
         .runOnMainSync(
             () -> {
               try {
-                testTransformer.start(composition, outputVideoFile.getAbsolutePath());
+                if (oldFilePath == null) {
+                  testTransformer.start(composition, outputVideoFile.getAbsolutePath());
+                } else {
+                  testTransformer.resume(
+                      composition, outputVideoFile.getAbsolutePath(), oldFilePath);
+                }
                 // Catch all exceptions to report. Exceptions thrown here and not caught will NOT
                 // propagate.
               } catch (Exception e) {
@@ -462,7 +492,7 @@
   }
 
   private static void logTimeoutDiagnostics() {
-    Log.e(TAG, "Effect debug traces at timeout: " + DebugTraceUtil.generateTrace());
+    Log.e(TAG, "Effect debug traces at timeout: " + DebugTraceUtil.generateTraceSummary());
     Log.e(TAG, "Thread state at timeout:");
     Set<Map.Entry<Thread, StackTraceElement[]>> entries = Thread.getAllStackTraces().entrySet();
     for (Map.Entry<Thread, StackTraceElement[]> threadAndStackTraceElements : entries) {
diff --git a/tests/tests/mediaediting/src/android/media/mediaediting/cts/VideoDecodingWrapper.java b/tests/tests/mediaediting/src/android/media/mediaediting/cts/VideoDecodingWrapper.java
index ec5098c..9e45ae2 100644
--- a/tests/tests/mediaediting/src/android/media/mediaediting/cts/VideoDecodingWrapper.java
+++ b/tests/tests/mediaediting/src/android/media/mediaediting/cts/VideoDecodingWrapper.java
@@ -16,10 +16,10 @@
 
 package android.media.mediaediting.cts;
 
+import static androidx.media3.common.C.MEDIA_CODEC_PRIORITY_NON_REALTIME;
 import static androidx.media3.common.util.Assertions.checkNotNull;
 import static androidx.media3.common.util.Assertions.checkState;
 import static androidx.media3.common.util.Assertions.checkStateNotNull;
-import static android.media.mediaediting.cts.AndroidTestUtil.MEDIA_CODEC_PRIORITY_NON_REALTIME;
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
@@ -33,14 +33,18 @@
 import android.media.MediaFormat;
 import android.os.Handler;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.media3.common.MimeTypes;
 import androidx.media3.common.util.ConditionVariable;
+import androidx.media3.common.util.UnstableApi;
 import androidx.media3.common.util.Util;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
 /** A wrapper for decoding a video using {@link MediaCodec}. */
-/* package */ final class VideoDecodingWrapper implements AutoCloseable {
+@UnstableApi
+@RequiresApi(21)
+public final class VideoDecodingWrapper implements AutoCloseable {
 
   private static final int IMAGE_AVAILABLE_TIMEOUT_MS = 10_000;