diff --git a/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java b/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java index af3bc33fb8..747eccb088 100644 --- a/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java +++ b/libraries/effect/src/androidTest/java/androidx/media3/effect/GlEffectsFrameProcessorPixelTest.java @@ -18,7 +18,6 @@ package androidx.media3.effect; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.effect.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; -import static androidx.media3.effect.FrameProcessorTestUtil.decodeOneFrame; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import static java.util.Arrays.asList; @@ -38,6 +37,7 @@ import androidx.media3.common.FrameInfo; import androidx.media3.common.FrameProcessingException; import androidx.media3.common.FrameProcessor; import androidx.media3.common.SurfaceInfo; +import androidx.media3.test.utils.DecodeOneFrameTestUtil; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import java.util.List; @@ -465,11 +465,11 @@ public final class GlEffectsFrameProcessorPixelTest { DebugViewProvider.NONE, ColorInfo.SDR_BT709_LIMITED, /* releaseFramesAutomatically= */ true)); - decodeOneFrame( + DecodeOneFrameTestUtil.decodeOneAssetFileFrame( INPUT_MP4_ASSET_STRING, - new FrameProcessorTestUtil.Listener() { + new DecodeOneFrameTestUtil.Listener() { @Override - public void onVideoMediaFormatExtracted(MediaFormat mediaFormat) { + public void onContainerExtracted(MediaFormat mediaFormat) { glEffectsFrameProcessor.setInputFrameInfo( new FrameInfo( mediaFormat.getInteger(MediaFormat.KEY_WIDTH), @@ -480,7 +480,7 @@ public final class GlEffectsFrameProcessorPixelTest { } @Override - public void onVideoMediaFormatRead(MediaFormat mediaFormat) { + public void onFrameDecoded(MediaFormat mediaFormat) { // Do nothing. } }, diff --git a/libraries/effect/src/androidTest/java/androidx/media3/effect/FrameProcessorTestUtil.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DecodeOneFrameTestUtil.java similarity index 69% rename from libraries/effect/src/androidTest/java/androidx/media3/effect/FrameProcessorTestUtil.java rename to libraries/test_utils/src/main/java/androidx/media3/test/utils/DecodeOneFrameTestUtil.java index 527e4a64eb..5d2acf4c0d 100644 --- a/libraries/effect/src/androidTest/java/androidx/media3/effect/FrameProcessorTestUtil.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DecodeOneFrameTestUtil.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.media3.effect; +package androidx.media3.test.utils; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; @@ -28,52 +28,98 @@ import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaFormat; import android.view.Surface; -import androidx.media3.common.FrameProcessor; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; import java.nio.ByteBuffer; import org.checkerframework.checker.nullness.qual.Nullable; -/** Utilities for instrumentation tests for {@link FrameProcessor}. */ -public class FrameProcessorTestUtil { +/** Utilities for decoding a frame for tests. */ +@UnstableApi +public class DecodeOneFrameTestUtil { /** Listener for decoding events. */ - interface Listener { + public interface Listener { /** Called when the video {@link MediaFormat} is extracted from the container. */ - void onVideoMediaFormatExtracted(MediaFormat mediaFormat); + void onContainerExtracted(MediaFormat mediaFormat); - /** Called when the video {@link MediaFormat} is read by the decoder from the byte stream. */ - void onVideoMediaFormatRead(MediaFormat mediaFormat); + /** + * Called when the video {@link MediaFormat} is read by the decoder from the byte stream, after + * a frame is decoded. + */ + void onFrameDecoded(MediaFormat mediaFormat); } /** Timeout for dequeueing buffers from the codec, in microseconds. */ private static final int DEQUEUE_TIMEOUT_US = 5_000_000; /** - * Decodes one frame from the {@code assetFilePath} and renders it to the {@code surface}. + * Reads and decodes one frame from the {@code cacheFilePath} and renders it to the {@code + * surface}. + * + * @param cacheFilePath The path to the file in the cache directory. + * @param listener A {@link Listener} implementation. + * @param surface The {@link Surface} to render the decoded frame to, {@code null} if the decoded + * frame is not needed. + */ + public static void decodeOneCacheFileFrame( + String cacheFilePath, Listener listener, @Nullable Surface surface) throws Exception { + MediaExtractor mediaExtractor = new MediaExtractor(); + try { + mediaExtractor.setDataSource(cacheFilePath); + decodeOneFrame(mediaExtractor, listener, surface); + } finally { + mediaExtractor.release(); + } + } + + /** + * Reads and decodes one frame from the {@code assetFilePath} and renders it to the {@code + * surface}. * * @param assetFilePath The path to the file in the asset directory. * @param listener A {@link Listener} implementation. * @param surface The {@link Surface} to render the decoded frame to, {@code null} if the decoded * frame is not needed. */ - public static void decodeOneFrame( + public static void decodeOneAssetFileFrame( String assetFilePath, Listener listener, @Nullable Surface surface) throws Exception { + MediaExtractor mediaExtractor = new MediaExtractor(); + Context context = getApplicationContext(); + try (AssetFileDescriptor afd = context.getAssets().openFd(assetFilePath)) { + mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + decodeOneFrame(mediaExtractor, listener, surface); + } finally { + mediaExtractor.release(); + } + } + + /** + * Reads and decodes one frame from the {@code mediaExtractor} and renders it to the {@code + * surface}. + * + * @param mediaExtractor The {@link MediaExtractor} with a {@link + * MediaExtractor#setDataSource(String) data source set}. + * @param listener A {@link Listener} implementation. + * @param surface The {@link Surface} to render the decoded frame to, {@code null} if the decoded + * frame is not needed. + */ + private static void decodeOneFrame( + MediaExtractor mediaExtractor, Listener listener, @Nullable Surface surface) + throws Exception { // Set up the extractor to read the first video frame and get its format. if (surface == null) { // Creates a placeholder surface. surface = new Surface(new SurfaceTexture(/* texName= */ 0)); } - MediaExtractor mediaExtractor = new MediaExtractor(); @Nullable MediaCodec mediaCodec = null; @Nullable MediaFormat mediaFormat = null; - Context context = getApplicationContext(); - try (AssetFileDescriptor afd = context.getAssets().openFd(assetFilePath)) { - mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + + try { for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { mediaFormat = mediaExtractor.getTrackFormat(i); - listener.onVideoMediaFormatExtracted(checkNotNull(mediaFormat)); + listener.onContainerExtracted(checkNotNull(mediaFormat)); mediaExtractor.selectTrack(i); break; } @@ -113,7 +159,7 @@ public class FrameProcessorTestUtil { do { outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); if (!decoderFormatRead && outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - listener.onVideoMediaFormatRead(mediaCodec.getOutputFormat()); + listener.onFrameDecoded(mediaCodec.getOutputFormat()); decoderFormatRead = true; } assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); @@ -121,12 +167,11 @@ public class FrameProcessorTestUtil { || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); } finally { - mediaExtractor.release(); if (mediaCodec != null) { mediaCodec.release(); } } } - private FrameProcessorTestUtil() {} + private DecodeOneFrameTestUtil() {} } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetHdrEditingTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetHdrEditingTransformationTest.java index bb9001e2ef..9a32a70d68 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetHdrEditingTransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/SetHdrEditingTransformationTest.java @@ -16,20 +16,26 @@ package androidx.media3.transformer.mh; import static androidx.media3.common.MimeTypes.VIDEO_H265; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_1080P_1_SECOND_HDR10_VIDEO_SDR_CONTAINER; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_1080P_4_SECOND_HDR10; import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; import static com.google.common.truth.Truth.assertThat; import android.content.Context; +import android.media.MediaFormat; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.ColorInfo; import androidx.media3.common.MediaItem; +import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.common.util.Util; +import androidx.media3.test.utils.DecodeOneFrameTestUtil; import androidx.media3.transformer.EncoderUtil; import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationRequest; +import androidx.media3.transformer.TransformationTestResult; import androidx.media3.transformer.Transformer; import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.test.core.app.ApplicationProvider; @@ -62,9 +68,11 @@ public class SetHdrEditingTransformationTest { .build(); try { - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); + TransformationTestResult transformationTestResult = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); + checkHasColorTransfer(transformationTestResult, C.COLOR_TRANSFER_ST2084); return; } catch (TransformationException exception) { assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); @@ -74,7 +82,6 @@ public class SetHdrEditingTransformationTest { .hasCauseThat() .hasMessageThat() .isEqualTo("HDR editing and tone mapping not supported under API 31."); - return; } } @@ -99,9 +106,11 @@ public class SetHdrEditingTransformationTest { .build()) .build(); - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); + TransformationTestResult transformationTestResult = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); + checkHasColorTransfer(transformationTestResult, C.COLOR_TRANSFER_ST2084); } @Test @@ -141,9 +150,11 @@ public class SetHdrEditingTransformationTest { .build(); try { - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); + TransformationTestResult transformationTestResult = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); + checkHasColorTransfer(transformationTestResult, C.COLOR_TRANSFER_SDR); } catch (TransformationException exception) { assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); // TODO(b/245364266): After fixing the bug, replace the API version check with a check that @@ -197,4 +208,30 @@ public class SetHdrEditingTransformationTest { private static boolean deviceSupportsHdrEditing(String mimeType, ColorInfo colorInfo) { return !EncoderUtil.getSupportedEncoderNamesForHdrEditing(mimeType, colorInfo).isEmpty(); } + + private static void checkHasColorTransfer( + TransformationTestResult transformationTestResult, @C.ColorTransfer int expectedColorTransfer) + throws Exception { + if (Util.SDK_INT < 29) { + // Skipping on this API version due to lack of support for MediaFormat#getInteger, which is + // required for MediaFormatUtil#getColorInfo. + return; + } + DecodeOneFrameTestUtil.decodeOneCacheFileFrame( + checkNotNull(transformationTestResult.filePath), + new DecodeOneFrameTestUtil.Listener() { + @Override + public void onContainerExtracted(MediaFormat mediaFormat) { + @Nullable ColorInfo extractedColor = MediaFormatUtil.getColorInfo(mediaFormat); + assertThat(checkNotNull(extractedColor).colorTransfer).isEqualTo(expectedColorTransfer); + } + + @Override + public void onFrameDecoded(MediaFormat mediaFormat) { + @Nullable ColorInfo decodedColor = MediaFormatUtil.getColorInfo(mediaFormat); + assertThat(checkNotNull(decodedColor).colorTransfer).isEqualTo(expectedColorTransfer); + } + }, + /* surface= */ null); + } }