diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 67522152b8..50d84cb958 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -89,7 +89,16 @@ public final class GlUtil { private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; // https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt private static final String EXTENSION_YUV_TARGET = "GL_EXT_YUV_target"; - + // https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt + private static final String EXTENSION_COLORSPACE_BT2020_PQ = "EGL_EXT_gl_colorspace_bt2020_pq"; + // https://registry.khronos.org/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt + private static final int EGL_GL_COLORSPACE_KHR = 0x309D; + // https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt + private static final int EGL_GL_COLORSPACE_BT2020_PQ_EXT = 0x3340; + private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ = + new int[] { + EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_PQ_EXT, EGL14.EGL_NONE, EGL14.EGL_NONE + }; private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE}; /** Class only contains static methods. */ @@ -216,6 +225,13 @@ public final class GlUtil { return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET); } + /** Returns whether {@value #EXTENSION_COLORSPACE_BT2020_PQ} is supported. */ + public static boolean isBt2020PqExtensionSupported() { + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_COLORSPACE_BT2020_PQ); + } + /** Returns an initialized default {@link EGLDisplay}. */ @RequiresApi(17) public static EGLDisplay createEglDisplay() throws GlException { @@ -258,30 +274,47 @@ public final class GlUtil { /** * Creates a new {@link EGLSurface} wrapping the specified {@code surface}. * - *
The {@link EGLSurface} will configure with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and - * OpenGL ES 2.0. + *
The {@link EGLSurface} will configure with OpenGL ES 2.0. * * @param eglDisplay The {@link EGLDisplay} to attach the surface to. * @param surface The surface to wrap; must be a surface, surface texture or surface holder. - */ - @RequiresApi(17) - public static EGLSurface createEglSurface(EGLDisplay eglDisplay, Object surface) - throws GlException { - return Api17.createEglSurface(eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888); - } - - /** - * Creates a new {@link EGLSurface} wrapping the specified {@code surface}. - * - * @param eglDisplay The {@link EGLDisplay} to attach the surface to. - * @param surface The surface to wrap; must be a surface, surface texture or surface holder. - * @param configAttributes The attributes to configure EGL with. Accepts {@link - * #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}. + * @param colorTransfer The {@linkplain C.ColorTransfer color transfer characteristics} to which + * the {@code surface} is configured. The only accepted values are {@link + * C#COLOR_TRANSFER_SDR}, {@link C#COLOR_TRANSFER_HLG} and {@link C#COLOR_TRANSFER_ST2084}. + * @param isEncoderInputSurface Whether the {@code surface} is the input surface of an encoder. */ @RequiresApi(17) public static EGLSurface createEglSurface( - EGLDisplay eglDisplay, Object surface, int[] configAttributes) throws GlException { - return Api17.createEglSurface(eglDisplay, surface, configAttributes); + EGLDisplay eglDisplay, + Object surface, + @C.ColorTransfer int colorTransfer, + boolean isEncoderInputSurface) + throws GlException { + int[] configAttributes; + int[] windowAttributes; + if (colorTransfer == C.COLOR_TRANSFER_SDR) { + configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_8888; + windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE; + } else if (colorTransfer == C.COLOR_TRANSFER_ST2084) { + configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_1010102; + if (isEncoderInputSurface) { + // Outputting BT2020 PQ with EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ to an encoder causes + // the encoder to incorrectly switch to full range color, even if the encoder is configured + // with limited range color, because EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ sets full range + // color output, and GL windowAttributes overrides encoder settings. + windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE; + } else { + // TODO(b/262259999) HDR10 PQ content looks dark on the screen. + windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ; + } + } else if (colorTransfer == C.COLOR_TRANSFER_HLG) { + checkArgument(isEncoderInputSurface, "Outputting HLG to the screen is not supported."); + configAttributes = EGL_CONFIG_ATTRIBUTES_RGBA_1010102; + windowAttributes = EGL_WINDOW_SURFACE_ATTRIBUTES_NONE; + } else { + throw new IllegalArgumentException("Unsupported color transfer: " + colorTransfer); + } + return Api17.createEglSurface(eglDisplay, surface, configAttributes, windowAttributes); } /** @@ -670,13 +703,14 @@ public final class GlUtil { @DoNotInline public static EGLSurface createEglSurface( - EGLDisplay eglDisplay, Object surface, int[] configAttributes) throws GlException { + EGLDisplay eglDisplay, Object surface, int[] configAttributes, int[] windowAttributes) + throws GlException { EGLSurface eglSurface = EGL14.eglCreateWindowSurface( eglDisplay, getEglConfig(eglDisplay, configAttributes), surface, - EGL_WINDOW_SURFACE_ATTRIBUTES_NONE, + windowAttributes, /* offset= */ 0); checkEglException("Error creating surface"); return eglSurface; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index 9198254ad6..d303e94080 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -23,6 +23,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.Bundleable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -38,6 +39,96 @@ import org.checkerframework.dataflow.qual.Pure; */ public final class ColorInfo implements Bundleable { + /** + * Builds {@link ColorInfo} instances. + * + *
Use {@link ColorInfo#buildUpon} to obtain a builder representing an existing {@link + * ColorInfo}. + */ + public static final class Builder { + private @C.ColorSpace int colorSpace; + private @C.ColorRange int colorRange; + private @C.ColorTransfer int colorTransfer; + @Nullable private byte[] hdrStaticInfo; + + /** Creates a new instance with default values. */ + public Builder() { + colorSpace = Format.NO_VALUE; + colorRange = Format.NO_VALUE; + colorTransfer = Format.NO_VALUE; + } + + /** Creates a new instance to build upon the provided {@link ColorInfo}. */ + private Builder(ColorInfo colorInfo) { + this.colorSpace = colorInfo.colorSpace; + this.colorRange = colorInfo.colorRange; + this.colorTransfer = colorInfo.colorTransfer; + this.hdrStaticInfo = colorInfo.hdrStaticInfo; + } + + /** + * Sets the color space. + * + *
Valid values are {@link C#COLOR_SPACE_BT601}, {@link C#COLOR_SPACE_BT709}, {@link + * C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown. + * + * @param colorSpace The color space. The default value is {@link Format#NO_VALUE}. + * @return This {@code Builder}. + */ + @CanIgnoreReturnValue + public Builder setColorSpace(@C.ColorSpace int colorSpace) { + this.colorSpace = colorSpace; + return this; + } + + /** + * Sets the color range. + * + *
Valid values are {@link C#COLOR_RANGE_LIMITED}, {@link C#COLOR_RANGE_FULL} or {@link + * Format#NO_VALUE} if unknown. + * + * @param colorRange The color range. The default value is {@link Format#NO_VALUE}. + * @return This {@code Builder}. + */ + @CanIgnoreReturnValue + public Builder setColorRange(@C.ColorRange int colorRange) { + this.colorRange = colorRange; + return this; + } + + /** + * Sets the color transfer. + * + *
Valid values are {@link C#COLOR_TRANSFER_LINEAR}, {@link C#COLOR_TRANSFER_HLG}, {@link + * C#COLOR_TRANSFER_ST2084}, {@link C#COLOR_TRANSFER_SDR} or {@link Format#NO_VALUE} if unknown. + * + * @param colorTransfer The color transfer. The default value is {@link Format#NO_VALUE}. + * @return This {@code Builder}. + */ + @CanIgnoreReturnValue + public Builder setColorTransfer(@C.ColorTransfer int colorTransfer) { + this.colorTransfer = colorTransfer; + return this; + } + + /** + * Sets the HdrStaticInfo as defined in CTA-861.3. + * + * @param hdrStaticInfo The HdrStaticInfo. The default value is {@code null}. + * @return This {@code Builder}. + */ + @CanIgnoreReturnValue + public Builder setHdrStaticInfo(@Nullable byte[] hdrStaticInfo) { + this.hdrStaticInfo = hdrStaticInfo; + return this; + } + + /** Builds a new {@link ColorInfo} instance. */ + public ColorInfo build() { + return new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); + } + } + /** Color info representing SDR BT.709 limited range, which is a common SDR video color format. */ public static final ColorInfo SDR_BT709_LIMITED = new ColorInfo( @@ -124,7 +215,9 @@ public final class ColorInfo implements Bundleable { * @param colorRange The color range of the video. * @param colorTransfer The color transfer characteristics of the video. * @param hdrStaticInfo HdrStaticInfo as defined in CTA-861.3, or null if none specified. + * @deprecated Use {@link Builder}. */ + @Deprecated public ColorInfo( @C.ColorSpace int colorSpace, @C.ColorRange int colorRange, @@ -136,6 +229,11 @@ public final class ColorInfo implements Bundleable { this.hdrStaticInfo = hdrStaticInfo; } + /** Returns a {@link Builder} initialized with the values of this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + /** * Returns whether this instance is valid. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 423a8cb70f..8b31e23ba6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1885,6 +1885,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } + ColorInfo inputColorInfo; + ColorInfo outputColorInfo; + if (inputFormat.colorInfo != null) { + inputColorInfo = inputFormat.colorInfo; + outputColorInfo = + inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + // SurfaceView only supports BT2020 PQ input, converting HLG to PQ. + ? inputColorInfo.buildUpon().setColorTransfer(C.COLOR_TRANSFER_ST2084).build() + : inputColorInfo; + } else { + inputColorInfo = ColorInfo.SDR_BT709_LIMITED; + outputColorInfo = ColorInfo.SDR_BT709_LIMITED; + } + // Playback thread handler. handler = Util.createHandlerForCurrentLooper(); try { @@ -1895,10 +1909,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { renderer.context, checkNotNull(videoEffects), DebugViewProvider.NONE, - inputFormat.colorInfo != null - ? inputFormat.colorInfo - : ColorInfo.SDR_BT709_LIMITED, - /* outputColorInfo= */ ColorInfo.SDR_BT709_LIMITED, + inputColorInfo, + outputColorInfo, /* releaseFramesAutomatically= */ false, /* executor= */ handler::post, new FrameProcessor.Listener() { diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/FinalMatrixTextureProcessorWrapper.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/FinalMatrixTextureProcessorWrapper.java index 98d5bf990c..6b6fc50d41 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/FinalMatrixTextureProcessorWrapper.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/FinalMatrixTextureProcessorWrapper.java @@ -340,15 +340,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; SurfaceInfo outputSurfaceInfo = this.outputSurfaceInfo; @Nullable EGLSurface outputEglSurface = this.outputEglSurface; if (outputEglSurface == null) { - boolean outputTransferIsHdr = ColorInfo.isTransferHdr(outputColorInfo); - outputEglSurface = GlUtil.createEglSurface( eglDisplay, outputSurfaceInfo.surface, - outputTransferIsHdr - ? GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_1010102 - : GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888); + outputColorInfo.colorTransfer, + // Frames are only released automatically when outputting to an encoder. + /* isEncoderInputSurface= */ releaseFramesAutomatically); @Nullable SurfaceView debugSurfaceView = @@ -356,7 +354,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; outputSurfaceInfo.width, outputSurfaceInfo.height); if (debugSurfaceView != null && !Util.areEqual(this.debugSurfaceView, debugSurfaceView)) { debugSurfaceViewWrapper = - new SurfaceViewWrapper(eglDisplay, eglContext, outputTransferIsHdr, debugSurfaceView); + new SurfaceViewWrapper( + eglDisplay, eglContext, ColorInfo.isTransferHdr(outputColorInfo), debugSurfaceView); } this.debugSurfaceView = debugSurfaceView; } @@ -474,13 +473,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } if (eglSurface == null) { + // Screen output supports only BT.2020 PQ (ST2084). eglSurface = GlUtil.createEglSurface( eglDisplay, surface, - useHdr - ? GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_1010102 - : GlUtil.EGL_CONFIG_ATTRIBUTES_RGBA_8888); + useHdr ? C.COLOR_TRANSFER_ST2084 : C.COLOR_TRANSFER_SDR, + /* isEncoderInputSurface= */ false); } EGLSurface eglSurface = this.eglSurface; GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, width, height); diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessor.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessor.java index 534b8fde76..9c638bdfa8 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessor.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffectsFrameProcessor.java @@ -62,6 +62,10 @@ public final class GlEffectsFrameProcessor implements FrameProcessor { * *
Using HDR {@code inputColorInfo} or {@code outputColorInfo} requires OpenGL ES 3.0. * + *
If outputting HDR content to a display, {@code EGL_GL_COLORSPACE_BT2020_PQ_EXT} is + * required, and {@link ColorInfo#colorTransfer outputColorInfo.colorTransfer} must be {@link + * C#COLOR_TRANSFER_ST2084}. + * *
Pass a {@link MoreExecutors#directExecutor() direct listenerExecutor} if invoking the
* {@code listener} on {@link GlEffectsFrameProcessor}'s internal thread is desired.
*/
@@ -161,6 +165,19 @@ public final class GlEffectsFrameProcessor implements FrameProcessor {
EGLContext eglContext = GlUtil.createEglContext(eglDisplay, openGlVersion, configAttributes);
GlUtil.createFocusedPlaceholderEglSurface(eglContext, eglDisplay, configAttributes);
+ // Not releaseFramesAutomatically means outputting to a display surface. HDR display surfaces
+ // require the BT2020 PQ GL extension.
+ if (!releaseFramesAutomatically && ColorInfo.isTransferHdr(outputColorInfo)) {
+ // Display hardware supports PQ only.
+ checkArgument(outputColorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084);
+ if (Util.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.
+ throw new FrameProcessingException("BT.2020 PQ OpenGL output isn't supported.");
+ }
+ }
+
ImmutableList