HDR: Assert output C.ColorTransfer for tests.

To confirm that tone mapping did or did not happen.

PiperOrigin-RevId: 476354606
This commit is contained in:
huangdarwin 2022-09-23 13:33:28 +00:00 committed by Marc Baechinger
parent dc9fa4f463
commit 87fd51a39b
3 changed files with 115 additions and 33 deletions

View File

@ -18,7 +18,6 @@ package androidx.media3.effect;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.effect.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; 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 androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@ -38,6 +37,7 @@ import androidx.media3.common.FrameInfo;
import androidx.media3.common.FrameProcessingException; import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.FrameProcessor; import androidx.media3.common.FrameProcessor;
import androidx.media3.common.SurfaceInfo; import androidx.media3.common.SurfaceInfo;
import androidx.media3.test.utils.DecodeOneFrameTestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
@ -465,11 +465,11 @@ public final class GlEffectsFrameProcessorPixelTest {
DebugViewProvider.NONE, DebugViewProvider.NONE,
ColorInfo.SDR_BT709_LIMITED, ColorInfo.SDR_BT709_LIMITED,
/* releaseFramesAutomatically= */ true)); /* releaseFramesAutomatically= */ true));
decodeOneFrame( DecodeOneFrameTestUtil.decodeOneAssetFileFrame(
INPUT_MP4_ASSET_STRING, INPUT_MP4_ASSET_STRING,
new FrameProcessorTestUtil.Listener() { new DecodeOneFrameTestUtil.Listener() {
@Override @Override
public void onVideoMediaFormatExtracted(MediaFormat mediaFormat) { public void onContainerExtracted(MediaFormat mediaFormat) {
glEffectsFrameProcessor.setInputFrameInfo( glEffectsFrameProcessor.setInputFrameInfo(
new FrameInfo( new FrameInfo(
mediaFormat.getInteger(MediaFormat.KEY_WIDTH), mediaFormat.getInteger(MediaFormat.KEY_WIDTH),
@ -480,7 +480,7 @@ public final class GlEffectsFrameProcessorPixelTest {
} }
@Override @Override
public void onVideoMediaFormatRead(MediaFormat mediaFormat) { public void onFrameDecoded(MediaFormat mediaFormat) {
// Do nothing. // Do nothing.
} }
}, },

View File

@ -14,7 +14,7 @@
* limitations under the License. * 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.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
@ -28,52 +28,98 @@ import android.media.MediaCodec;
import android.media.MediaExtractor; import android.media.MediaExtractor;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.view.Surface; import android.view.Surface;
import androidx.media3.common.FrameProcessor;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
/** Utilities for instrumentation tests for {@link FrameProcessor}. */ /** Utilities for decoding a frame for tests. */
public class FrameProcessorTestUtil { @UnstableApi
public class DecodeOneFrameTestUtil {
/** Listener for decoding events. */ /** Listener for decoding events. */
interface Listener { public interface Listener {
/** Called when the video {@link MediaFormat} is extracted from the container. */ /** 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. */ /** Timeout for dequeueing buffers from the codec, in microseconds. */
private static final int DEQUEUE_TIMEOUT_US = 5_000_000; 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 assetFilePath The path to the file in the asset directory.
* @param listener A {@link Listener} implementation. * @param listener A {@link Listener} implementation.
* @param surface The {@link Surface} to render the decoded frame to, {@code null} if the decoded * @param surface The {@link Surface} to render the decoded frame to, {@code null} if the decoded
* frame is not needed. * frame is not needed.
*/ */
public static void decodeOneFrame( public static void decodeOneAssetFileFrame(
String assetFilePath, Listener listener, @Nullable Surface surface) throws Exception { 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. // Set up the extractor to read the first video frame and get its format.
if (surface == null) { if (surface == null) {
// Creates a placeholder surface. // Creates a placeholder surface.
surface = new Surface(new SurfaceTexture(/* texName= */ 0)); surface = new Surface(new SurfaceTexture(/* texName= */ 0));
} }
MediaExtractor mediaExtractor = new MediaExtractor();
@Nullable MediaCodec mediaCodec = null; @Nullable MediaCodec mediaCodec = null;
@Nullable MediaFormat mediaFormat = null; @Nullable MediaFormat mediaFormat = null;
Context context = getApplicationContext();
try (AssetFileDescriptor afd = context.getAssets().openFd(assetFilePath)) { try {
mediaExtractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {
if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) { if (MimeTypes.isVideo(mediaExtractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME))) {
mediaFormat = mediaExtractor.getTrackFormat(i); mediaFormat = mediaExtractor.getTrackFormat(i);
listener.onVideoMediaFormatExtracted(checkNotNull(mediaFormat)); listener.onContainerExtracted(checkNotNull(mediaFormat));
mediaExtractor.selectTrack(i); mediaExtractor.selectTrack(i);
break; break;
} }
@ -113,7 +159,7 @@ public class FrameProcessorTestUtil {
do { do {
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US); outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, DEQUEUE_TIMEOUT_US);
if (!decoderFormatRead && outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { if (!decoderFormatRead && outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
listener.onVideoMediaFormatRead(mediaCodec.getOutputFormat()); listener.onFrameDecoded(mediaCodec.getOutputFormat());
decoderFormatRead = true; decoderFormatRead = true;
} }
assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER); assertThat(outputBufferIndex).isNotEqualTo(MediaCodec.INFO_TRY_AGAIN_LATER);
@ -121,12 +167,11 @@ public class FrameProcessorTestUtil {
|| outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED); || outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED);
mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true); mediaCodec.releaseOutputBuffer(outputBufferIndex, /* render= */ true);
} finally { } finally {
mediaExtractor.release();
if (mediaCodec != null) { if (mediaCodec != null) {
mediaCodec.release(); mediaCodec.release();
} }
} }
} }
private FrameProcessorTestUtil() {} private DecodeOneFrameTestUtil() {}
} }

View File

@ -16,20 +16,26 @@
package androidx.media3.transformer.mh; package androidx.media3.transformer.mh;
import static androidx.media3.common.MimeTypes.VIDEO_H265; 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_1_SECOND_HDR10_VIDEO_SDR_CONTAINER;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_1080P_4_SECOND_HDR10; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_1080P_4_SECOND_HDR10;
import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.media.MediaFormat;
import android.net.Uri; import android.net.Uri;
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.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.util.MediaFormatUtil;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.test.utils.DecodeOneFrameTestUtil;
import androidx.media3.transformer.EncoderUtil; import androidx.media3.transformer.EncoderUtil;
import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.TransformationTestResult;
import androidx.media3.transformer.Transformer; import androidx.media3.transformer.Transformer;
import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.media3.transformer.TransformerAndroidTestRunner;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -62,9 +68,11 @@ public class SetHdrEditingTransformationTest {
.build(); .build();
try { try {
new TransformerAndroidTestRunner.Builder(context, transformer) TransformationTestResult transformationTestResult =
.build() new TransformerAndroidTestRunner.Builder(context, transformer)
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); .build()
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10)));
checkHasColorTransfer(transformationTestResult, C.COLOR_TRANSFER_ST2084);
return; return;
} catch (TransformationException exception) { } catch (TransformationException exception) {
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
@ -74,7 +82,6 @@ public class SetHdrEditingTransformationTest {
.hasCauseThat() .hasCauseThat()
.hasMessageThat() .hasMessageThat()
.isEqualTo("HDR editing and tone mapping not supported under API 31."); .isEqualTo("HDR editing and tone mapping not supported under API 31.");
return;
} }
} }
@ -99,9 +106,11 @@ public class SetHdrEditingTransformationTest {
.build()) .build())
.build(); .build();
new TransformerAndroidTestRunner.Builder(context, transformer) TransformationTestResult transformationTestResult =
.build() new TransformerAndroidTestRunner.Builder(context, transformer)
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); .build()
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10)));
checkHasColorTransfer(transformationTestResult, C.COLOR_TRANSFER_ST2084);
} }
@Test @Test
@ -141,9 +150,11 @@ public class SetHdrEditingTransformationTest {
.build(); .build();
try { try {
new TransformerAndroidTestRunner.Builder(context, transformer) TransformationTestResult transformationTestResult =
.build() new TransformerAndroidTestRunner.Builder(context, transformer)
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10))); .build()
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10)));
checkHasColorTransfer(transformationTestResult, C.COLOR_TRANSFER_SDR);
} catch (TransformationException exception) { } catch (TransformationException exception) {
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
// TODO(b/245364266): After fixing the bug, replace the API version check with a check that // 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) { private static boolean deviceSupportsHdrEditing(String mimeType, ColorInfo colorInfo) {
return !EncoderUtil.getSupportedEncoderNamesForHdrEditing(mimeType, colorInfo).isEmpty(); 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);
}
} }