mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Tone map in CompositionPlayer if surface can't display HDR
PiperOrigin-RevId: 740290183
This commit is contained in:
parent
a1738f96f9
commit
6034a3c3d6
@ -228,6 +228,16 @@ public final class GlUtil {
|
|||||||
return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET);
|
return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns whether the given {@link C.ColorTransfer} is supported. */
|
||||||
|
public static boolean isColorTransferSupported(@C.ColorTransfer int colorTransfer) {
|
||||||
|
if (colorTransfer == C.COLOR_TRANSFER_ST2084) {
|
||||||
|
return GlUtil.isBt2020PqExtensionSupported();
|
||||||
|
} else if (colorTransfer == C.COLOR_TRANSFER_HLG) {
|
||||||
|
return GlUtil.isBt2020HlgExtensionSupported();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns whether {@link #EXTENSION_COLORSPACE_BT2020_PQ} is supported. */
|
/** Returns whether {@link #EXTENSION_COLORSPACE_BT2020_PQ} is supported. */
|
||||||
public static boolean isBt2020PqExtensionSupported() {
|
public static boolean isBt2020PqExtensionSupported() {
|
||||||
// On API<33, the system cannot display PQ content correctly regardless of whether BT2020 PQ
|
// On API<33, the system cannot display PQ content correctly regardless of whether BT2020 PQ
|
||||||
|
@ -48,7 +48,9 @@ import androidx.media3.common.VideoFrameProcessor;
|
|||||||
import androidx.media3.common.VideoGraph;
|
import androidx.media3.common.VideoGraph;
|
||||||
import androidx.media3.common.VideoSize;
|
import androidx.media3.common.VideoSize;
|
||||||
import androidx.media3.common.util.Clock;
|
import androidx.media3.common.util.Clock;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
import androidx.media3.common.util.HandlerWrapper;
|
import androidx.media3.common.util.HandlerWrapper;
|
||||||
|
import androidx.media3.common.util.Log;
|
||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.TimedValueQueue;
|
import androidx.media3.common.util.TimedValueQueue;
|
||||||
import androidx.media3.common.util.TimestampIterator;
|
import androidx.media3.common.util.TimestampIterator;
|
||||||
@ -268,6 +270,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
|||||||
@IntDef({STATE_CREATED, STATE_INITIALIZED, STATE_RELEASED})
|
@IntDef({STATE_CREATED, STATE_INITIALIZED, STATE_RELEASED})
|
||||||
private @interface State {}
|
private @interface State {}
|
||||||
|
|
||||||
|
private static final String TAG = "PlaybackVidGraphWrapper";
|
||||||
private static final int STATE_CREATED = 0;
|
private static final int STATE_CREATED = 0;
|
||||||
private static final int STATE_INITIALIZED = 1;
|
private static final int STATE_INITIALIZED = 1;
|
||||||
private static final int STATE_RELEASED = 2;
|
private static final int STATE_RELEASED = 2;
|
||||||
@ -507,17 +510,26 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
|||||||
ColorInfo outputColorInfo;
|
ColorInfo outputColorInfo;
|
||||||
if (requestOpenGlToneMapping) {
|
if (requestOpenGlToneMapping) {
|
||||||
outputColorInfo = ColorInfo.SDR_BT709_LIMITED;
|
outputColorInfo = ColorInfo.SDR_BT709_LIMITED;
|
||||||
|
} else if (inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG
|
||||||
|
&& Util.SDK_INT < 34
|
||||||
|
&& GlUtil.isBt2020PqExtensionSupported()) {
|
||||||
|
// PQ SurfaceView output is supported from API 33, but HLG output is supported from API
|
||||||
|
// 34. Therefore, convert HLG to PQ if PQ is supported, so that HLG input can be displayed
|
||||||
|
// properly on API 33.
|
||||||
|
outputColorInfo =
|
||||||
|
inputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build();
|
||||||
|
// Force OpenGL tone mapping if the GL extension required to output HDR colors is not
|
||||||
|
// available. OpenGL tone mapping is only supported on API 29+.
|
||||||
|
} else if (!GlUtil.isColorTransferSupported(inputColorInfo.colorTransfer)
|
||||||
|
&& Util.SDK_INT >= 29) {
|
||||||
|
Log.w(
|
||||||
|
TAG,
|
||||||
|
Util.formatInvariant(
|
||||||
|
"Color transfer %d is not supported. Falling back to OpenGl tone mapping.",
|
||||||
|
inputColorInfo.colorTransfer));
|
||||||
|
outputColorInfo = ColorInfo.SDR_BT709_LIMITED;
|
||||||
} else {
|
} else {
|
||||||
outputColorInfo = inputColorInfo;
|
outputColorInfo = inputColorInfo;
|
||||||
if (outputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG && Util.SDK_INT < 34) {
|
|
||||||
// PQ SurfaceView output is supported from API 33, but HLG output is supported from API
|
|
||||||
// 34.
|
|
||||||
// Therefore, convert HLG to PQ below API 34, so that HLG input can be displayed properly
|
|
||||||
// on
|
|
||||||
// API 33.
|
|
||||||
outputColorInfo =
|
|
||||||
outputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
handler = clock.createHandler(checkStateNotNull(Looper.myLooper()), /* callback= */ null);
|
handler = clock.createHandler(checkStateNotNull(Looper.myLooper()), /* callback= */ null);
|
||||||
try {
|
try {
|
||||||
|
@ -28,6 +28,7 @@ import static androidx.media3.common.util.Util.SDK_INT;
|
|||||||
import static androidx.media3.test.utils.TestUtil.retrieveTrackFormat;
|
import static androidx.media3.test.utils.TestUtil.retrieveTrackFormat;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.util.concurrent.Futures.immediateFuture;
|
import static com.google.common.util.concurrent.Futures.immediateFuture;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
import static org.junit.Assume.assumeFalse;
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -43,14 +44,20 @@ import android.os.Build;
|
|||||||
import androidx.annotation.Nullable;
|
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.DebugViewProvider;
|
||||||
|
import androidx.media3.common.Effect;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.GlObjectsProvider;
|
import androidx.media3.common.GlObjectsProvider;
|
||||||
import androidx.media3.common.GlTextureInfo;
|
import androidx.media3.common.GlTextureInfo;
|
||||||
import androidx.media3.common.Metadata;
|
import androidx.media3.common.Metadata;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
|
import androidx.media3.common.VideoCompositorSettings;
|
||||||
|
import androidx.media3.common.VideoGraph;
|
||||||
|
import androidx.media3.common.VideoGraph.Listener;
|
||||||
import androidx.media3.common.util.GlRect;
|
import androidx.media3.common.util.GlRect;
|
||||||
import androidx.media3.common.util.GlUtil;
|
import androidx.media3.common.util.GlUtil;
|
||||||
import androidx.media3.common.util.Log;
|
import androidx.media3.common.util.Log;
|
||||||
|
import androidx.media3.common.util.NullableType;
|
||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.effect.ByteBufferGlEffect;
|
import androidx.media3.effect.ByteBufferGlEffect;
|
||||||
@ -59,6 +66,7 @@ import androidx.media3.effect.GlEffect;
|
|||||||
import androidx.media3.effect.GlShaderProgram;
|
import androidx.media3.effect.GlShaderProgram;
|
||||||
import androidx.media3.effect.PassthroughShaderProgram;
|
import androidx.media3.effect.PassthroughShaderProgram;
|
||||||
import androidx.media3.effect.ScaleAndRotateTransformation;
|
import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||||
|
import androidx.media3.effect.SingleInputVideoGraph;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
|
||||||
import androidx.media3.muxer.MuxerException;
|
import androidx.media3.muxer.MuxerException;
|
||||||
@ -66,6 +74,7 @@ import androidx.media3.test.utils.BitmapPixelTestUtil;
|
|||||||
import androidx.media3.test.utils.FakeExtractorOutput;
|
import androidx.media3.test.utils.FakeExtractorOutput;
|
||||||
import androidx.media3.test.utils.FakeTrackOutput;
|
import androidx.media3.test.utils.FakeTrackOutput;
|
||||||
import androidx.media3.test.utils.VideoDecodingWrapper;
|
import androidx.media3.test.utils.VideoDecodingWrapper;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
@ -75,8 +84,12 @@ import java.io.FileWriter;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@ -1090,6 +1103,87 @@ public final class AndroidTestUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A {@link VideoGraph.Factory} that records test interactions. */
|
||||||
|
public static class TestVideoGraphFactory implements VideoGraph.Factory {
|
||||||
|
|
||||||
|
private final VideoGraph.Factory singleInputVideoGraphFactory;
|
||||||
|
|
||||||
|
@Nullable private ColorInfo outputColorInfo;
|
||||||
|
|
||||||
|
public TestVideoGraphFactory() {
|
||||||
|
singleInputVideoGraphFactory = new SingleInputVideoGraph.Factory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VideoGraph create(
|
||||||
|
Context context,
|
||||||
|
ColorInfo outputColorInfo,
|
||||||
|
DebugViewProvider debugViewProvider,
|
||||||
|
Listener listener,
|
||||||
|
Executor listenerExecutor,
|
||||||
|
VideoCompositorSettings videoCompositorSettings,
|
||||||
|
List<Effect> compositionEffects,
|
||||||
|
long initialTimestampOffsetUs,
|
||||||
|
boolean renderFramesAutomatically) {
|
||||||
|
this.outputColorInfo = outputColorInfo;
|
||||||
|
return singleInputVideoGraphFactory.create(
|
||||||
|
context,
|
||||||
|
outputColorInfo,
|
||||||
|
debugViewProvider,
|
||||||
|
listener,
|
||||||
|
listenerExecutor,
|
||||||
|
videoCompositorSettings,
|
||||||
|
compositionEffects,
|
||||||
|
initialTimestampOffsetUs,
|
||||||
|
renderFramesAutomatically);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs the given task and blocks until it completes, or timeoutSeconds has elapsed. */
|
||||||
|
public static void runAsyncTaskAndWait(ThrowingRunnable task, int timeoutSeconds)
|
||||||
|
throws TimeoutException, InterruptedException {
|
||||||
|
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
AtomicReference<@NullableType Exception> unexpectedExceptionReference =
|
||||||
|
new AtomicReference<>();
|
||||||
|
InstrumentationRegistry.getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
// Catch all exceptions to report. Exceptions thrown here and not caught will NOT
|
||||||
|
// propagate.
|
||||||
|
} catch (Exception e) {
|
||||||
|
unexpectedExceptionReference.set(e);
|
||||||
|
} finally {
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block here until timeout reached or latch is counted down.
|
||||||
|
if (!countDownLatch.await(timeoutSeconds, SECONDS)) {
|
||||||
|
throw new TimeoutException("Timed out after " + timeoutSeconds + " seconds.");
|
||||||
|
}
|
||||||
|
@Nullable Exception unexpectedException = unexpectedExceptionReference.get();
|
||||||
|
if (unexpectedException != null) {
|
||||||
|
throw new IllegalStateException(unexpectedException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsMultipleInputs() {
|
||||||
|
return singleInputVideoGraphFactory.supportsMultipleInputs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ColorInfo getOutputColorInfo() {
|
||||||
|
return outputColorInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A type that can be used to succinctly wrap throwing {@link Runnable} objects. */
|
||||||
|
public interface ThrowingRunnable {
|
||||||
|
void run() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the GL objects needed to set up a GL environment including an {@link EGLDisplay} and an
|
* Creates the GL objects needed to set up a GL environment including an {@link EGLDisplay} and an
|
||||||
* {@link EGLContext}.
|
* {@link EGLContext}.
|
||||||
|
@ -24,6 +24,8 @@ import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
|||||||
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
|
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_COLOR_TEST_1080P_HLG10;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_COLOR_TEST_1080P_HLG10;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_270;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_270;
|
||||||
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceDoesNotSupportHdrColorTransfer;
|
||||||
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsHdrColorTransfer;
|
||||||
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
@ -104,6 +106,7 @@ public class FrameExtractorHdrTest {
|
|||||||
@Test
|
@Test
|
||||||
public void extractFrame_oneFrameHlgWithHdrOutput_returnsHlgFrame() throws Exception {
|
public void extractFrame_oneFrameHlgWithHdrOutput_returnsHlgFrame() throws Exception {
|
||||||
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
||||||
|
assumeDeviceSupportsHdrColorTransfer(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
||||||
// HLG Bitmaps are only supported on API 34+.
|
// HLG Bitmaps are only supported on API 34+.
|
||||||
assumeTrue(SDK_INT >= 34);
|
assumeTrue(SDK_INT >= 34);
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
@ -134,6 +137,33 @@ public class FrameExtractorHdrTest {
|
|||||||
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
|
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void extractFrame_oneFrameHlgWithHdrDisplayUnsupported_returnsSdrFrame() throws Exception {
|
||||||
|
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
||||||
|
assumeDeviceDoesNotSupportHdrColorTransfer(
|
||||||
|
testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
||||||
|
// HLG Bitmaps are only supported on API 34+.
|
||||||
|
assumeTrue(SDK_INT >= 34);
|
||||||
|
frameExtractor =
|
||||||
|
new ExperimentalFrameExtractor(
|
||||||
|
context,
|
||||||
|
new ExperimentalFrameExtractor.Configuration.Builder()
|
||||||
|
.setExtractHdrFrames(true)
|
||||||
|
.build());
|
||||||
|
frameExtractor.setMediaItem(
|
||||||
|
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri), /* effects= */ ImmutableList.of());
|
||||||
|
|
||||||
|
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
|
||||||
|
frameExtractor.getFrame(/* positionMs= */ 0);
|
||||||
|
ExperimentalFrameExtractor.Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||||
|
Bitmap actualBitmap = frame.bitmap;
|
||||||
|
Bitmap expectedBitmap = readBitmap(TONE_MAP_HLG_TO_SDR_PNG_ASSET_PATH);
|
||||||
|
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
|
||||||
|
|
||||||
|
assertThat(frame.presentationTimeMs).isEqualTo(0);
|
||||||
|
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
extractFrame_changeMediaItemFromHdrToSdrWithToneMapping_extractsFrameFromTheCorrectItem()
|
extractFrame_changeMediaItemFromHdrToSdrWithToneMapping_extractsFrameFromTheCorrectItem()
|
||||||
|
@ -16,14 +16,17 @@
|
|||||||
package androidx.media3.transformer.mh;
|
package androidx.media3.transformer.mh;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped;
|
import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped;
|
||||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.opengl.EGLDisplay;
|
||||||
import androidx.media3.common.ColorInfo;
|
import androidx.media3.common.ColorInfo;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.util.GlUtil;
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.GlUtil.GlException;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
|
||||||
import androidx.media3.transformer.EncoderUtil;
|
import androidx.media3.transformer.EncoderUtil;
|
||||||
@ -96,5 +99,49 @@ public final class HdrCapabilitiesUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes that the device supports HDR editing for the given {@code colorInfo}.
|
||||||
|
*
|
||||||
|
* @throws AssumptionViolatedException if the device does not support HDR editing.
|
||||||
|
*/
|
||||||
|
public static void assumeDeviceSupportsHdrColorTransfer(String testId, Format format)
|
||||||
|
throws JSONException, IOException, GlException {
|
||||||
|
checkStateNotNull(format.colorInfo);
|
||||||
|
// Required to ensure EGL extensions are initialised.
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
|
||||||
|
if (!GlUtil.isColorTransferSupported(format.colorInfo.colorTransfer)) {
|
||||||
|
String skipReason =
|
||||||
|
"HDR display not supported for sampleMimeType "
|
||||||
|
+ format.sampleMimeType
|
||||||
|
+ " and colorInfo "
|
||||||
|
+ format.colorInfo;
|
||||||
|
recordTestSkipped(getApplicationContext(), testId, skipReason);
|
||||||
|
throw new AssumptionViolatedException(skipReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assumes that the device does not support HDR editing for the given {@code colorInfo}.
|
||||||
|
*
|
||||||
|
* @throws AssumptionViolatedException if the device does support HDR editing.
|
||||||
|
*/
|
||||||
|
public static void assumeDeviceDoesNotSupportHdrColorTransfer(String testId, Format format)
|
||||||
|
throws JSONException, IOException, GlException {
|
||||||
|
checkStateNotNull(format.colorInfo);
|
||||||
|
// Required to ensure EGL extensions are initialised.
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
|
||||||
|
if (GlUtil.isColorTransferSupported(format.colorInfo.colorTransfer)) {
|
||||||
|
String skipReason =
|
||||||
|
"HDR display is supported for sampleMimeType "
|
||||||
|
+ format.sampleMimeType
|
||||||
|
+ " and colorInfo "
|
||||||
|
+ format.colorInfo;
|
||||||
|
recordTestSkipped(getApplicationContext(), testId, skipReason);
|
||||||
|
throw new AssumptionViolatedException(skipReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private HdrCapabilitiesUtil() {}
|
private HdrCapabilitiesUtil() {}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,175 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.mh;
|
||||||
|
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_720P_4_SECOND_HDR10;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_BT2020_SDR;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_COLOR_TEST_1080P_HLG10;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.TestVideoGraphFactory.runAsyncTaskAndWait;
|
||||||
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceDoesNotSupportHdrColorTransfer;
|
||||||
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsHdrColorTransfer;
|
||||||
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.ColorInfo;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.VideoGraph;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.exoplayer.video.PlaybackVideoGraphWrapper;
|
||||||
|
import androidx.media3.exoplayer.video.VideoFrameReleaseControl;
|
||||||
|
import androidx.media3.exoplayer.video.VideoSink;
|
||||||
|
import androidx.media3.transformer.AndroidTestUtil.TestVideoGraphFactory;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TestName;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Instrumentation tests for {@link PlaybackVideoGraphWrapper}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class PlaybackVideoGraphWrapperTest {
|
||||||
|
|
||||||
|
private static final int TEST_TIMEOUT_SECOND = 1;
|
||||||
|
|
||||||
|
@Rule public final TestName testName = new TestName();
|
||||||
|
private String testId;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUpTestId() {
|
||||||
|
testId = testName.getMethodName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialize_sdrInput_retainsSdr() throws Exception {
|
||||||
|
Format inputFormat = MP4_ASSET_BT2020_SDR.videoFormat;
|
||||||
|
TestVideoGraphFactory testVideoGraphFactory = new TestVideoGraphFactory();
|
||||||
|
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
|
||||||
|
createPlaybackVideoGraphWrapper(testVideoGraphFactory);
|
||||||
|
VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
|
||||||
|
|
||||||
|
runAsyncTaskAndWait(() -> sink.initialize(inputFormat), TEST_TIMEOUT_SECOND);
|
||||||
|
|
||||||
|
assertThat(testVideoGraphFactory.getOutputColorInfo()).isEqualTo(inputFormat.colorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialize_hdr10InputUnsupported_toneMapsToSdr() throws Exception {
|
||||||
|
Format inputFormat = MP4_ASSET_720P_4_SECOND_HDR10.videoFormat;
|
||||||
|
assumeDeviceSupportsOpenGlToneMapping(testId, inputFormat);
|
||||||
|
assumeDeviceDoesNotSupportHdrColorTransfer(testId, inputFormat);
|
||||||
|
TestVideoGraphFactory testVideoGraphFactory = new TestVideoGraphFactory();
|
||||||
|
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
|
||||||
|
createPlaybackVideoGraphWrapper(testVideoGraphFactory);
|
||||||
|
VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
|
||||||
|
|
||||||
|
runAsyncTaskAndWait(() -> sink.initialize(inputFormat), TEST_TIMEOUT_SECOND);
|
||||||
|
|
||||||
|
assertThat(testVideoGraphFactory.getOutputColorInfo()).isEqualTo(ColorInfo.SDR_BT709_LIMITED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialize_hlgInputUnsupported_toneMapsToSdr() throws Exception {
|
||||||
|
Format inputFormat = MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat;
|
||||||
|
assumeDeviceSupportsOpenGlToneMapping(testId, inputFormat);
|
||||||
|
assumeDeviceDoesNotSupportHdrColorTransfer(testId, inputFormat);
|
||||||
|
TestVideoGraphFactory testVideoGraphFactory = new TestVideoGraphFactory();
|
||||||
|
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
|
||||||
|
createPlaybackVideoGraphWrapper(testVideoGraphFactory);
|
||||||
|
VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
|
||||||
|
|
||||||
|
runAsyncTaskAndWait(() -> sink.initialize(inputFormat), TEST_TIMEOUT_SECOND);
|
||||||
|
|
||||||
|
ColorInfo expectedColorInfo;
|
||||||
|
// HLG is converted to PQ on API 33.
|
||||||
|
if (Util.SDK_INT < 34 && GlUtil.isBt2020PqExtensionSupported()) {
|
||||||
|
expectedColorInfo =
|
||||||
|
inputFormat.colorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build();
|
||||||
|
} else {
|
||||||
|
expectedColorInfo = ColorInfo.SDR_BT709_LIMITED;
|
||||||
|
}
|
||||||
|
assertThat(testVideoGraphFactory.getOutputColorInfo()).isEqualTo(expectedColorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialize_hdr10InputSupported_retainsHdr() throws Exception {
|
||||||
|
Format inputFormat = MP4_ASSET_720P_4_SECOND_HDR10.videoFormat;
|
||||||
|
assumeDeviceSupportsHdrColorTransfer(testId, inputFormat);
|
||||||
|
TestVideoGraphFactory testVideoGraphFactory = new TestVideoGraphFactory();
|
||||||
|
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
|
||||||
|
createPlaybackVideoGraphWrapper(testVideoGraphFactory);
|
||||||
|
VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
|
||||||
|
|
||||||
|
runAsyncTaskAndWait(() -> sink.initialize(inputFormat), TEST_TIMEOUT_SECOND);
|
||||||
|
|
||||||
|
assertThat(testVideoGraphFactory.getOutputColorInfo()).isEqualTo(inputFormat.colorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialize_hlgInputSupported_retainsHdr() throws Exception {
|
||||||
|
Format inputFormat = MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat;
|
||||||
|
assumeDeviceSupportsHdrColorTransfer(testId, inputFormat);
|
||||||
|
TestVideoGraphFactory testVideoGraphFactory = new TestVideoGraphFactory();
|
||||||
|
PlaybackVideoGraphWrapper playbackVideoGraphWrapper =
|
||||||
|
createPlaybackVideoGraphWrapper(testVideoGraphFactory);
|
||||||
|
VideoSink sink = playbackVideoGraphWrapper.getSink(/* inputIndex= */ 0);
|
||||||
|
|
||||||
|
runAsyncTaskAndWait(() -> sink.initialize(inputFormat), TEST_TIMEOUT_SECOND);
|
||||||
|
|
||||||
|
assertThat(testVideoGraphFactory.getOutputColorInfo()).isEqualTo(inputFormat.colorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper(
|
||||||
|
VideoGraph.Factory videoGraphFactory) {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
return new PlaybackVideoGraphWrapper.Builder(context, createVideoFrameReleaseControl())
|
||||||
|
.setVideoGraphFactory(videoGraphFactory)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VideoFrameReleaseControl createVideoFrameReleaseControl() {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
VideoFrameReleaseControl.FrameTimingEvaluator frameTimingEvaluator =
|
||||||
|
new VideoFrameReleaseControl.FrameTimingEvaluator() {
|
||||||
|
@Override
|
||||||
|
public boolean shouldForceReleaseFrame(long earlyUs, long elapsedSinceLastReleaseUs) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldDropFrame(
|
||||||
|
long earlyUs, long elapsedRealtimeUs, boolean isLastFrame) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldIgnoreFrame(
|
||||||
|
long earlyUs,
|
||||||
|
long positionUs,
|
||||||
|
long elapsedRealtimeUs,
|
||||||
|
boolean isLastFrame,
|
||||||
|
boolean treatDroppedBuffersAsSkipped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new VideoFrameReleaseControl(
|
||||||
|
context, frameTimingEvaluator, /* allowedJoiningTimeMs= */ 0);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user