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