diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index 1360abf4e0..c32b5edf05 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -366,11 +366,23 @@ public final class GlUtil { return eglSurface; } + /** + * Returns the {@link EGL14#EGL_CONTEXT_CLIENT_VERSION} of the current context. + * + *

Returns {@code 0} if no {@link EGLContext} {@linkplain #createFocusedPlaceholderEglSurface + * is focused}. + */ + @RequiresApi(17) + public static long getContextMajorVersion() throws GlException { + return Api17.getContextMajorVersion(); + } + /** * Returns a newly created sync object and inserts it into the GL command stream. * - *

Returns {@code 0} if the operation failed or the {@link EGLContext} version is less than - * 3.0. + *

Returns {@code 0} if the operation failed, no {@link EGLContext} {@linkplain + * #createFocusedPlaceholderEglSurface is focused}, or the focused {@link EGLContext} version is + * less than 3.0. */ @RequiresApi(17) public static long createGlSyncFence() throws GlException { diff --git a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java index c129d95647..e445877310 100644 --- a/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java +++ b/libraries/effect/src/main/java/androidx/media3/effect/DefaultVideoFrameProcessor.java @@ -19,6 +19,7 @@ import static androidx.media3.common.util.Assertions.checkArgument; 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 androidx.media3.common.util.Util.SDK_INT; import static androidx.media3.effect.DebugTraceUtil.EVENT_VFP_RECEIVE_END_OF_INPUT; import static androidx.media3.effect.DebugTraceUtil.EVENT_VFP_REGISTER_NEW_INPUT_STREAM; import static androidx.media3.effect.DebugTraceUtil.EVENT_VFP_SIGNAL_ENDED; @@ -637,18 +638,20 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { ColorInfo.isTransferHdr(outputColorInfo) ? GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_1010102 : GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888; - int openGlVersion = - ColorInfo.isTransferHdr(inputColorInfo) || ColorInfo.isTransferHdr(outputColorInfo) ? 3 : 2; EGLContext eglContext = - glObjectsProvider.createEglContext(eglDisplay, openGlVersion, configAttributes); - glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext, eglDisplay); + createFocusedEglContextWithFallback(glObjectsProvider, eglDisplay, configAttributes); + if ((ColorInfo.isTransferHdr(inputColorInfo) || ColorInfo.isTransferHdr(outputColorInfo)) + && GlUtil.getContextMajorVersion() != 3) { + throw new VideoFrameProcessingException( + "OpenGL ES 3.0 context support is required for HDR input or output."); + } // Not renderFramesAutomatically means outputting to a display surface. HDR display surfaces // require the BT2020 PQ GL extension. if (!renderFramesAutomatically && ColorInfo.isTransferHdr(outputColorInfo)) { // Display hardware supports PQ only. checkArgument(outputColorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084); - if (Util.SDK_INT < 33 || !GlUtil.isBt2020PqExtensionSupported()) { + if (SDK_INT < 33 || !GlUtil.isBt2020PqExtensionSupported()) { GlUtil.destroyEglContext(eglDisplay, eglContext); // On API<33, the system cannot display PQ content correctly regardless of whether BT2020 PQ // GL extension is supported. @@ -882,6 +885,43 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor { } } + /** Creates an OpenGL ES 3.0 context if possible, and an OpenGL ES 2.0 context otherwise. */ + private static EGLContext createFocusedEglContextWithFallback( + GlObjectsProvider glObjectsProvider, EGLDisplay eglDisplay, int[] configAttributes) + throws GlUtil.GlException { + if (SDK_INT < 29) { + return createFocusedEglContext( + glObjectsProvider, eglDisplay, /* openGlVersion= */ 2, configAttributes); + } + + try { + return createFocusedEglContext( + glObjectsProvider, eglDisplay, /* openGlVersion= */ 3, configAttributes); + } catch (GlUtil.GlException e) { + return createFocusedEglContext( + glObjectsProvider, eglDisplay, /* openGlVersion= */ 2, configAttributes); + } + } + + /** + * Creates an {@link EGLContext} and focus it using a {@linkplain + * GlObjectsProvider#createFocusedPlaceholderEglSurface placeholder EGL Surface}. + */ + private static EGLContext createFocusedEglContext( + GlObjectsProvider glObjectsProvider, + EGLDisplay eglDisplay, + int openGlVersion, + int[] configAttributes) + throws GlUtil.GlException { + EGLContext eglContext = + glObjectsProvider.createEglContext(eglDisplay, openGlVersion, configAttributes); + // Some OpenGL ES 3.0 contexts returned from createEglContext may throw EGL_BAD_MATCH when being + // used to createFocusedPlaceHolderEglSurface, despite GL documentation suggesting the contexts, + // if successfully created, are valid. Check early whether the context is really valid. + glObjectsProvider.createFocusedPlaceholderEglSurface(eglContext, eglDisplay); + return eglContext; + } + private static final class InputStreamInfo { public final @InputType int inputType; public final List effects; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java index d542a1f46b..20e4eab792 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/HdrEditingTest.java @@ -48,6 +48,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.Test; import org.junit.runner.RunWith; @@ -268,13 +269,15 @@ public final class HdrEditingTest { assertThat(isToneMappingFallbackApplied.get()).isTrue(); assertFileHasColorTransfer(context, exportTestResult.filePath, C.COLOR_TRANSFER_SDR); } catch (ExportException exception) { - if (exception.getCause() != null - && (Objects.equals( - exception.getCause().getMessage(), - "Decoding HDR is not supported on this device.") - || Objects.equals( - exception.getCause().getMessage(), "Device lacks YUV extension support."))) { - return; + if (exception.getCause() != null) { + @Nullable String message = exception.getCause().getMessage(); + if (message != null + && (Objects.equals(message, "Decoding HDR is not supported on this device.") + || message.contains( + "OpenGL ES 3.0 context support is required for HDR input or output.") + || Objects.equals(message, "Device lacks YUV extension support."))) { + return; + } } throw exception; } @@ -330,13 +333,15 @@ public final class HdrEditingTest { assertThat(isToneMappingFallbackApplied.get()).isTrue(); assertFileHasColorTransfer(context, exportTestResult.filePath, C.COLOR_TRANSFER_SDR); } catch (ExportException exception) { - if (exception.getCause() != null - && (Objects.equals( - exception.getCause().getMessage(), - "Decoding HDR is not supported on this device.") - || Objects.equals( - exception.getCause().getMessage(), "Device lacks YUV extension support."))) { - return; + if (exception.getCause() != null) { + @Nullable String message = exception.getCause().getMessage(); + if (message != null + && (Objects.equals(message, "Decoding HDR is not supported on this device.") + || message.contains( + "OpenGL ES 3.0 context support is required for HDR input or output.") + || Objects.equals(message, "Device lacks YUV extension support."))) { + return; + } } throw exception; }