diff --git a/google3/third_party/java_src/android_libs/media/libraries/effect/src/androidTest/java/androidx/media3/effect/HslAdjustmentPixelTest.java b/google3/third_party/java_src/android_libs/media/libraries/effect/src/androidTest/java/androidx/media3/effect/HslAdjustmentPixelTest.java new file mode 100644 index 0000000000..faa81e49da --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/effect/src/androidTest/java/androidx/media3/effect/HslAdjustmentPixelTest.java @@ -0,0 +1,360 @@ +/* + * Copyright 2022 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 + * + * http://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.effect; + +import static androidx.media3.effect.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.util.Pair; +import androidx.media3.common.FrameProcessingException; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.util.GlUtil; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Pixel tests for {@link HslAdjustment}. + * + *

Expected images are taken from an emulator, so tests on different emulators or physical + * devices may fail. To test on other devices, please increase the {@link + * BitmapTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} and/or inspect the saved output bitmaps + * as recommended in {@link GlEffectsFrameProcessorPixelTest}. + */ +@RunWith(AndroidJUnit4.class) +public final class HslAdjustmentPixelTest { + public static final String ORIGINAL_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/original.png"; + public static final String MINIMUM_SATURATION_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/minimum_saturation.png"; + public static final String MAXIMUM_SATURATION_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/maximum_saturation.png"; + public static final String ROTATE_HUE_BY_NEGATIVE_90_DEGREES_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_negative_90_degrees.png"; + public static final String ROTATE_HUE_BY_60_DEGREES_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_60_degrees.png"; + public static final String DECREASE_LIGHTNESS_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/decrease_lightness.png"; + public static final String INCREASE_LIGHTNESS_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/increase_lightness.png"; + public static final String ADJUST_ALL_HSL_SETTINGS_PNG_ASSET_PATH = + "media/bitmap/sample_mp4_first_frame/linear_colors/adjust_all_hsl_settings.png"; + + private final Context context = getApplicationContext(); + + private @MonotonicNonNull EGLDisplay eglDisplay; + private @MonotonicNonNull EGLContext eglContext; + private @MonotonicNonNull SingleFrameGlTextureProcessor hslProcessor; + private @MonotonicNonNull EGLSurface placeholderEglSurface; + private int inputTexId; + private int outputTexId; + private int inputWidth; + private int inputHeight; + + @Before + public void createGlObjects() throws IOException, GlUtil.GlException { + eglDisplay = GlUtil.createEglDisplay(); + eglContext = GlUtil.createEglContext(eglDisplay); + Bitmap inputBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH); + inputWidth = inputBitmap.getWidth(); + inputHeight = inputBitmap.getHeight(); + placeholderEglSurface = GlUtil.createPlaceholderEglSurface(eglDisplay); + GlUtil.focusEglSurface(eglDisplay, eglContext, placeholderEglSurface, inputWidth, inputHeight); + inputTexId = BitmapTestUtil.createGlTextureFromBitmap(inputBitmap); + + outputTexId = + GlUtil.createTexture(inputWidth, inputHeight, /* useHighPrecisionColorComponents= */ false); + int frameBuffer = GlUtil.createFboForTexture(outputTexId); + GlUtil.focusFramebuffer( + eglDisplay, eglContext, placeholderEglSurface, frameBuffer, inputWidth, inputHeight); + } + + @After + public void release() throws GlUtil.GlException, FrameProcessingException { + if (hslProcessor != null) { + hslProcessor.release(); + } + GlUtil.destroyEglContext(eglDisplay, eglContext); + } + + @Test + public void drawFrame_noOpAdjustment_leavesFrameUnchanged() throws Exception { + String testId = "drawFrame_noOpAdjustment"; + HslAdjustment noOpAdjustment = new HslAdjustment.Builder().build(); + hslProcessor = noOpAdjustment.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_minimumSaturation_producesGrayFrame() throws Exception { + String testId = "drawFrame_minimumSaturation"; + HslAdjustment minimumSaturation = new HslAdjustment.Builder().adjustSaturation(-100).build(); + hslProcessor = minimumSaturation.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(MINIMUM_SATURATION_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_maximumSaturation_producesHighlySaturatedFrame() throws Exception { + String testId = "drawFrame_maximumSaturation"; + HslAdjustment maximumSaturation = new HslAdjustment.Builder().adjustSaturation(100).build(); + hslProcessor = maximumSaturation.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(MAXIMUM_SATURATION_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_rotateHueByNegative90Degrees_producesExpectedOutput() throws Exception { + String testId = "drawFrame_rotateHueByNegative90Degrees"; + HslAdjustment negativeHueRotation90Degrees = new HslAdjustment.Builder().adjustHue(-90).build(); + hslProcessor = negativeHueRotation90Degrees.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = + BitmapTestUtil.readBitmap(ROTATE_HUE_BY_NEGATIVE_90_DEGREES_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_rotateHueBy60Degrees_producesExpectedOutput() throws Exception { + String testId = "drawFrame_rotateHueBy60Degrees"; + HslAdjustment hueRotation60Degrees = new HslAdjustment.Builder().adjustHue(60).build(); + hslProcessor = hueRotation60Degrees.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_HUE_BY_60_DEGREES_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_rotateHueByNegative300Degrees_producesSameOutputAsRotateBy60DegreesHue() + throws Exception { + String testId = "drawFrame_rotateHueByNegative300Degrees"; + HslAdjustment hueRotation420Degrees = new HslAdjustment.Builder().adjustHue(-300).build(); + hslProcessor = hueRotation420Degrees.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_HUE_BY_60_DEGREES_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_rotateHueBy360Degrees_leavesFrameUnchanged() throws Exception { + String testId = "drawFrame_rotateHueBy360Degrees"; + HslAdjustment hueRotation360Degrees = new HslAdjustment.Builder().adjustHue(360).build(); + hslProcessor = hueRotation360Degrees.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_minimumLightness_producesBlackFrame() throws Exception { + String testId = "drawFrame_minimumLightness"; + HslAdjustment minimumLightness = new HslAdjustment.Builder().adjustLightness(-100).build(); + hslProcessor = minimumLightness.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = + BitmapTestUtil.createArgb8888BitmapWithSolidColor(inputWidth, inputHeight, Color.BLACK); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_decreaseLightness_producesDarkerFrame() throws Exception { + String testId = "drawFrame_decreaseLightness"; + HslAdjustment decreasedLightness = new HslAdjustment.Builder().adjustLightness(-50).build(); + hslProcessor = decreasedLightness.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(DECREASE_LIGHTNESS_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_increaseLightness_producesBrighterFrame() throws Exception { + String testId = "drawFrame_increaseLightness"; + HslAdjustment increasedLightness = new HslAdjustment.Builder().adjustLightness(50).build(); + hslProcessor = increasedLightness.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(INCREASE_LIGHTNESS_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_maximumLightness_producesWhiteFrame() throws Exception { + String testId = "drawFrame_maximumLightness"; + HslAdjustment maximumLightness = new HslAdjustment.Builder().adjustLightness(100).build(); + hslProcessor = maximumLightness.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = + BitmapTestUtil.createArgb8888BitmapWithSolidColor(inputWidth, inputHeight, Color.WHITE); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } + + @Test + public void drawFrame_adjustAllHslSettings_producesExpectedOutput() throws Exception { + String testId = "drawFrame_adjustAllHslSettings"; + HslAdjustment allHslSettingsAdjusted = + new HslAdjustment.Builder().adjustHue(60).adjustSaturation(30).adjustLightness(50).build(); + hslProcessor = allHslSettingsAdjusted.toGlTextureProcessor(context, /* useHdr= */ false); + Pair outputSize = hslProcessor.configure(inputWidth, inputHeight); + Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ADJUST_ALL_HSL_SETTINGS_PNG_ASSET_PATH); + + hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0); + Bitmap actualBitmap = + BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer( + outputSize.first, outputSize.second); + + BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory( + testId, /* bitmapLabel= */ "actual", actualBitmap); + float averagePixelAbsoluteDifference = + BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888( + expectedBitmap, actualBitmap, testId); + assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/assets/shaders/fragment_shader_hsl_es2.glsl b/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/assets/shaders/fragment_shader_hsl_es2.glsl new file mode 100644 index 0000000000..9c330f43d3 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/assets/shaders/fragment_shader_hsl_es2.glsl @@ -0,0 +1,77 @@ +#version 100 +// Copyright 2022 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 +// +// http://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. + +// ES 2 fragment shader that samples from a (non-external) texture with +// uTexSampler. It then converts the RGB color input into HSL and adjusts +// the Hue, Saturation, and Lightness and converts it then back to RGB. + +// We use the algorithm based on the work by Sam Hocevar, which optimizes +// for an efficient branchless RGB <-> HSL conversion. A blog post is +// at https://www.chilliant.com/rgb2hsv.html and it is further explained at +// http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv. + +precision highp float; +uniform sampler2D uTexSampler; +// uHueAdjustmentDegrees, uSaturationAdjustment, and uLightnessAdjustment +// are normalized to the unit interval [0, 1]. +uniform float uHueAdjustmentDegrees; +uniform float uSaturationAdjustment; +uniform float uLightnessAdjustment; +varying vec2 vTexSamplingCoord; + +const float epsilon = 1e-10; + +vec3 rgbToHcv(vec3 rgb) { + vec4 p = (rgb.g < rgb.b) + ? vec4(rgb.bg, -1.0, 2.0 / 3.0) + : vec4(rgb.gb, 0.0, -1.0 / 3.0); + vec4 q = (rgb.r < p.x) + ? vec4(p.xyw, rgb.r) + : vec4(rgb.r, p.yzx); + float c = q.x - min(q.w, q.y); + float h = abs((q.w - q.y) / (6.0 * c + epsilon) + q.z); + return vec3(h, c, q.x); +} + +vec3 rgbToHsl(vec3 rgb) { + vec3 hcv = rgbToHcv(rgb); + float l = hcv.z - hcv.y * 0.5; + float s = hcv.y / (1.0 - abs(l * 2.0 - 1.0) + epsilon); + return vec3(hcv.x, s, l); +} + +vec3 hueToRgb(float hue) { + float r = abs(hue * 6.0 - 3.0) - 1.0; + float g = 2.0 - abs(hue * 6.0 - 2.0); + float b = 2.0 - abs(hue * 6.0 - 4.0); + return clamp(vec3(r, g, b), 0.0, 1.0); +} + +vec3 hslToRgb(vec3 hsl) { + vec3 rgb = hueToRgb(hsl.x); + float c = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y; + return (rgb - 0.5) * c + hsl.z; +} + +void main() { + vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord); + vec3 hslColor = rgbToHsl(inputColor.rgb); + + hslColor.x = mod(hslColor.x + uHueAdjustmentDegrees, 1.0); + hslColor.y = clamp(hslColor.y + uSaturationAdjustment, 0.0, 1.0); + hslColor.z = clamp(hslColor.z + uLightnessAdjustment, 0.0, 1.0); + + gl_FragColor = vec4(hslToRgb(hslColor), inputColor.a); +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/java/androidx/media3/effect/HslAdjustment.java b/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/java/androidx/media3/effect/HslAdjustment.java new file mode 100644 index 0000000000..8f5a3a0400 --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/java/androidx/media3/effect/HslAdjustment.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 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 + * + * http://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.effect; + +import static com.google.android.exoplayer2.util.Assertions.checkArgument; + +import android.content.Context; +import androidx.media3.common.FrameProcessingException; +import com.google.errorprone.annotations.CanIgnoreReturnValue; + +/** Adjusts the HSL (Hue, Saturation, and Lightness) of a frame. */ +public class HslAdjustment implements GlEffect { + + /** A builder for {@code HslAdjustment} instances. */ + public static final class Builder { + private float hueAdjustment; + private float saturationAdjustment; + private float lightnessAdjustment; + + /** Creates a new instance with the default values. */ + public Builder() {} + + /** + * Rotates the hue of the frame by {@code hueAdjustmentDegrees}. + * + *

The Hue of the frame is defined in the interval of [0, 360] degrees. The actual degrees of + * hue adjustment applied is {@code hueAdjustmentDegrees % 360}. + * + * @param hueAdjustmentDegrees The hue adjustment in rotation degrees. The default value is + * {@code 0}, which means no change is applied. + */ + @CanIgnoreReturnValue + public Builder adjustHue(float hueAdjustmentDegrees) { + hueAdjustment = hueAdjustmentDegrees % 360; + return this; + } + + /** + * Adjusts the saturation of the frame by {@code saturationAdjustment}. + * + *

Saturation is defined in the interval of [0, 100] where a saturation of {@code 0} will + * generate a grayscale frame and a saturation of {@code 100} has a maximum separation between + * the colors. + * + * @param saturationAdjustment The difference of how much the saturation will be adjusted in + * either direction. Needs to be in the interval of [-100, 100] and the default value is + * {@code 0}, which means no change is applied. + */ + @CanIgnoreReturnValue + public Builder adjustSaturation(float saturationAdjustment) { + checkArgument( + -100 <= saturationAdjustment && saturationAdjustment <= 100, + "Can adjust the saturation by only 100 in either direction, but provided " + + saturationAdjustment); + this.saturationAdjustment = saturationAdjustment; + return this; + } + + /** + * Adjusts the lightness of the frame by {@code lightnessAdjustment}. + * + *

Lightness is defined in the interval of [0, 100] where a lightness of {@code 0} is a black + * frame and a lightness of {@code 100} is a white frame. + * + * @param lightnessAdjustment The difference by how much the lightness will be adjusted in + * either direction. Needs to be in the interval of [-100, 100] and the default value is + * {@code 0}, which means no change is applied. + */ + @CanIgnoreReturnValue + public Builder adjustLightness(float lightnessAdjustment) { + checkArgument( + -100 <= lightnessAdjustment && lightnessAdjustment <= 100, + "Can adjust the lightness by only 100 in either direction, but provided " + + lightnessAdjustment); + this.lightnessAdjustment = lightnessAdjustment; + return this; + } + + /** Creates a new {@link HslAdjustment} instance. */ + public HslAdjustment build() { + return new HslAdjustment(hueAdjustment, saturationAdjustment, lightnessAdjustment); + } + } + + /** Indicates the hue adjustment in degrees. */ + public final float hueAdjustmentDegrees; + /** Indicates the saturation adjustment. */ + public final float saturationAdjustment; + /** Indicates the lightness adjustment. */ + public final float lightnessAdjustment; + + private HslAdjustment( + float hueAdjustmentDegrees, float saturationAdjustment, float lightnessAdjustment) { + this.hueAdjustmentDegrees = hueAdjustmentDegrees; + this.saturationAdjustment = saturationAdjustment; + this.lightnessAdjustment = lightnessAdjustment; + } + + @Override + public HslProcessor toGlTextureProcessor(Context context, boolean useHdr) + throws FrameProcessingException { + return new HslProcessor(context, /* hslAdjustment= */ this, useHdr); + } +} diff --git a/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/java/androidx/media3/effect/HslProcessor.java b/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/java/androidx/media3/effect/HslProcessor.java new file mode 100644 index 0000000000..cc93cdddee --- /dev/null +++ b/google3/third_party/java_src/android_libs/media/libraries/effect/src/main/java/androidx/media3/effect/HslProcessor.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022 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 + * + * http://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.effect; + +import static com.google.android.exoplayer2.util.Assertions.checkArgument; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.util.Pair; +import androidx.media3.common.FrameProcessingException; +import com.google.android.exoplayer2.util.GlProgram; +import com.google.android.exoplayer2.util.GlUtil; +import java.io.IOException; + +/** Applies the {@link HslAdjustment} to each frame in the fragment shader. */ +/* package */ final class HslProcessor extends SingleFrameGlTextureProcessor { + private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl"; + private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_hsl_es2.glsl"; + + private final GlProgram glProgram; + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param hslAdjustment The {@link HslAdjustment} to apply to each frame in order. + * @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be + * in linear RGB BT.2020. If {@code false}, colors will be in gamma RGB BT.709. + * @throws FrameProcessingException If a problem occurs while reading shader files. + */ + public HslProcessor(Context context, HslAdjustment hslAdjustment, boolean useHdr) + throws FrameProcessingException { + super(useHdr); + // TODO(b/241241680): Check if HDR <-> HSL works the same or not. + checkArgument(!useHdr, "HDR is not yet supported."); + + try { + glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + } catch (IOException | GlUtil.GlException e) { + throw new FrameProcessingException(e); + } + + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. + glProgram.setBufferAttribute( + "aFramePosition", + GlUtil.getNormalizedCoordinateBounds(), + GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); + + float[] identityMatrix = new float[16]; + Matrix.setIdentityM(identityMatrix, /* smOffset= */ 0); + glProgram.setFloatsUniform("uTransformationMatrix", identityMatrix); + glProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix); + + // OpenGL operates in a [0, 1] unit range and thus we transform the HSL intervals into + // the unit interval as well. The hue is defined in the [0, 360] interval and saturation + // and lightness in the [0, 100] interval. + glProgram.setFloatUniform("uHueAdjustmentDegrees", hslAdjustment.hueAdjustmentDegrees / 360); + glProgram.setFloatUniform("uSaturationAdjustment", hslAdjustment.saturationAdjustment / 100); + glProgram.setFloatUniform("uLightnessAdjustment", hslAdjustment.lightnessAdjustment / 100); + } + + @Override + public Pair configure(int inputWidth, int inputHeight) { + return Pair.create(inputWidth, inputHeight); + } + + @Override + public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException { + try { + glProgram.use(); + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); + glProgram.bindAttributesAndUniforms(); + + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + } catch (GlUtil.GlException e) { + throw new FrameProcessingException(e, presentationTimeUs); + } + } +} diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/adjust_all_hsl_settings.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/adjust_all_hsl_settings.png new file mode 100644 index 0000000000..b5b0ad2c57 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/adjust_all_hsl_settings.png differ diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/decrease_lightness.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/decrease_lightness.png new file mode 100644 index 0000000000..00bda2f03e Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/decrease_lightness.png differ diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/increase_lightness.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/increase_lightness.png new file mode 100644 index 0000000000..395ef18621 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/increase_lightness.png differ diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/maximum_saturation.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/maximum_saturation.png new file mode 100644 index 0000000000..c45f342b69 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/maximum_saturation.png differ diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/minimum_saturation.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/minimum_saturation.png new file mode 100644 index 0000000000..1c66594312 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/minimum_saturation.png differ diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_60_degrees.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_60_degrees.png new file mode 100644 index 0000000000..6ee4c84a93 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_60_degrees.png differ diff --git a/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_negative_90_degrees.png b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_negative_90_degrees.png new file mode 100644 index 0000000000..e4165a2177 Binary files /dev/null and b/testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_negative_90_degrees.png differ