diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java index 4c19e2776b..47b4a875ca 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/AndroidTestUtil.java @@ -53,6 +53,9 @@ public final class AndroidTestUtil { */ public static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 1; + public static final String PNG_ASSET_URI_STRING = + "asset:///media/bitmap/input_images/media3test.png"; + public static final String MP4_ASSET_URI_STRING = "asset:///media/mp4/sample.mp4"; public static final Format MP4_ASSET_FORMAT = new Format.Builder() diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerEndToEndTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerEndToEndTest.java index 43442fdead..c6a87617ed 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerEndToEndTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/TransformerEndToEndTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.transformer; import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING; +import static com.google.android.exoplayer2.transformer.AndroidTestUtil.PNG_ASSET_URI_STRING; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; @@ -24,6 +25,7 @@ import android.content.Context; import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.effect.Presentation; @@ -41,6 +43,53 @@ public class TransformerEndToEndTest { private final Context context = ApplicationProvider.getApplicationContext(); + @Test + public void videoEditing_withImageInput_completesWithCorrectFrameCountAndDuration() + throws Exception { + String testId = "videoEditing_withImageInput_completesWithCorrectFrameCountAndDuration"; + Transformer transformer = new Transformer.Builder(context).build(); + ImmutableList videoEffects = ImmutableList.of(Presentation.createForHeight(480)); + Effects effects = new Effects(/* audioProcessors= */ ImmutableList.of(), videoEffects); + int expectedFrameCount = 40; + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(MediaItem.fromUri(PNG_ASSET_URI_STRING)) + .setDurationUs(C.MICROS_PER_SECOND) + .setFrameRate(expectedFrameCount) + .setEffects(effects) + .build(); + ExportTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, editedMediaItem); + + assertThat(result.exportResult.videoFrameCount).isEqualTo(expectedFrameCount); + // Expected timestamp of the last frame. + assertThat(result.exportResult.durationMs) + .isEqualTo((C.MILLIS_PER_SECOND / expectedFrameCount) * (expectedFrameCount - 1)); + } + + @Test + public void videoTranscoding_withImageInput_completesWithCorrectFrameCountAndDuration() + throws Exception { + String testId = "videoTranscoding_withImageInput_completesWithCorrectFrameCountAndDuration"; + Transformer transformer = new Transformer.Builder(context).build(); + int expectedFrameCount = 40; + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(MediaItem.fromUri(PNG_ASSET_URI_STRING)) + .setDurationUs(C.MICROS_PER_SECOND) + .setFrameRate(expectedFrameCount) + .build(); + ExportTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, editedMediaItem); + + assertThat(result.exportResult.videoFrameCount).isEqualTo(expectedFrameCount); + // Expected timestamp of the last frame. + assertThat(result.exportResult.durationMs) + .isEqualTo((C.MILLIS_PER_SECOND / expectedFrameCount) * (expectedFrameCount - 1)); + } + @Test public void videoEditing_completesWithConsistentFrameCount() throws Exception { Transformer transformer = diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/ExportTest.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/ExportTest.java index 788ea31377..f4345cbd8f 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/ExportTest.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/ExportTest.java @@ -253,22 +253,4 @@ public class ExportTest { .build() .run(testId, editedMediaItem); } - - @Test - public void exportImage() throws Exception { - // TODO(b/262693274): consider removing this test when Robolectric tests have been added for - // image input. - String testId = TAG + "_exportImage"; - Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = new Transformer.Builder(context).build(); - String imageUri = "https://storage.googleapis.com/exoplayer-test-media-1/jpg/london.jpg"; - EditedMediaItem editedMediaItem = - new EditedMediaItem.Builder(MediaItem.fromUri(imageUri)) - .setDurationUs(1_000_000) - .setFrameRate(30) - .build(); - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(testId, editedMediaItem); - } } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/ImageAssetLoaderTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/ImageAssetLoaderTest.java new file mode 100644 index 0000000000..f435ee2300 --- /dev/null +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/ImageAssetLoaderTest.java @@ -0,0 +1,152 @@ +/* + * Copyright 2023 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 + * + * http://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 com.google.android.exoplayer2.transformer; + +import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runLooperUntil; +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowSystemClock; + +/** Unit tests for {@link ExoPlayerAssetLoader}. */ +@RunWith(AndroidJUnit4.class) +public class ImageAssetLoaderTest { + + @Test + public void imageAssetLoader_callsListenerCallbacksInRightOrder() throws Exception { + HandlerThread assetLoaderThread = new HandlerThread("AssetLoaderThread"); + assetLoaderThread.start(); + Looper assetLoaderLooper = assetLoaderThread.getLooper(); + AtomicReference exceptionRef = new AtomicReference<>(); + AtomicBoolean isOutputFormatSet = new AtomicBoolean(); + AssetLoader.Listener listener = + new AssetLoader.Listener() { + + private volatile boolean isDurationSet; + private volatile boolean isTrackCountSet; + private volatile boolean isTrackAdded; + + @Override + public void onDurationUs(long durationUs) { + sleep(); + isDurationSet = true; + } + + @Override + public void onTrackCount(int trackCount) { + // Sleep to increase the chances of the test failing. + sleep(); + isTrackCountSet = true; + } + + @Override + public boolean onTrackAdded( + Format inputFormat, + @AssetLoader.SupportedOutputTypes int supportedOutputTypes, + long streamStartPositionUs, + long streamOffsetUs) { + if (!isTrackCountSet) { + exceptionRef.set( + new IllegalStateException("onTrackAdded() called before onTrackCount()")); + } + sleep(); + isTrackAdded = true; + return false; + } + + @Override + public SampleConsumer onOutputFormat(Format format) { + + if (!isDurationSet) { + exceptionRef.set( + new IllegalStateException("onTrackAdded() called before onDurationUs()")); + } else if (!isTrackAdded) { + exceptionRef.set( + new IllegalStateException("onOutputFormat() called before onTrackAdded()")); + } + isOutputFormatSet.set(true); + return new FakeSampleConsumer(); + } + + @Override + public void onError(ExportException e) { + exceptionRef.set(e); + } + + void sleep() { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + exceptionRef.set(e); + } + } + }; + AssetLoader assetLoader = getAssetLoader(assetLoaderLooper, listener); + + new Handler(assetLoaderLooper).post(assetLoader::start); + runLooperUntil( + Looper.myLooper(), + () -> { + ShadowSystemClock.advanceBy(Duration.ofMillis(10)); + return isOutputFormatSet.get() || exceptionRef.get() != null; + }); + + assertThat(exceptionRef.get()).isNull(); + } + + private static AssetLoader getAssetLoader(Looper looper, AssetLoader.Listener listener) { + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder( + MediaItem.fromUri("asset:///media/bitmap/input_images/media3test.png")) + .setDurationUs(1_000_000) + .setFrameRate(30) + .build(); + return new ImageAssetLoader.Factory(ApplicationProvider.getApplicationContext()) + .createAssetLoader(editedMediaItem, looper, listener); + } + + private static final class FakeSampleConsumer implements SampleConsumer { + + @Nullable + @Override + public DecoderInputBuffer getInputBuffer() { + return null; + } + + @Override + public void queueInputBuffer() {} + + @Override + public void queueInputBitmap(Bitmap inputBitmap, long durationUs, int frameRate) {} + + @Override + public void signalEndOfVideoInput() {} + } +}