Effect: support disabling color transfers when HDR->SDR tonemapping

also makes the setter more flexible by ignoring the value of the setter when the output is hdr rather than throwing (since all HDR content must be have a linear color space)

PiperOrigin-RevId: 627388436
This commit is contained in:
tofunmi 2024-04-23 08:00:03 -07:00 committed by Copybara-Service
parent f2276f613d
commit 73f614b14d
4 changed files with 86 additions and 17 deletions

View File

@ -335,10 +335,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
String fragmentShaderFilePath =
outputIsHdr
? FRAGMENT_SHADER_OETF_ES3_PATH
: FRAGMENT_SHADER_TRANSFORMATION_SDR_OETF_ES2_PATH;
if (!enableColorTransfers) {
fragmentShaderFilePath = FRAGMENT_SHADER_TRANSFORMATION_PATH;
}
: enableColorTransfers
? FRAGMENT_SHADER_TRANSFORMATION_SDR_OETF_ES2_PATH
: FRAGMENT_SHADER_TRANSFORMATION_PATH;
GlProgram glProgram = createGlProgram(context, vertexShaderFilePath, fragmentShaderFilePath);
@C.ColorTransfer int outputColorTransfer = outputColorInfo.colorTransfer;

View File

@ -113,7 +113,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
*
* <p>The default value is {@code true}.
*
* <p>If the input or output is HDR, this must be {@code true}.
* <p>If the output is HDR, this is ignored as the working color space must have a linear
* transfer function.
*
* <p>If all input and output content will be SDR, it's recommended to set this value to
* {@code false}. This is because 8-bit colors in SDR may result in color banding.
@ -348,7 +349,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
private final List<Effect> activeEffects;
private final Object lock;
private final boolean enableColorTransfers;
private final ColorInfo outputColorInfo;
private volatile @MonotonicNonNull FrameInfo nextInputFrameInfo;
@ -365,7 +365,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
Executor listenerExecutor,
FinalShaderProgramWrapper finalShaderProgramWrapper,
boolean renderFramesAutomatically,
boolean enableColorTransfers,
ColorInfo outputColorInfo) {
this.context = context;
this.glObjectsProvider = glObjectsProvider;
@ -378,7 +377,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
this.renderFramesAutomatically = renderFramesAutomatically;
this.activeEffects = new ArrayList<>();
this.lock = new Object();
this.enableColorTransfers = enableColorTransfers;
this.outputColorInfo = outputColorInfo;
this.finalShaderProgramWrapper = finalShaderProgramWrapper;
this.intermediateGlShaderPrograms = new ArrayList<>();
@ -692,10 +690,14 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
.setColorTransfer(C.COLOR_TRANSFER_LINEAR)
.setHdrStaticInfo(null)
.build();
ColorInfo intermediateColorInfo =
ColorInfo.isTransferHdr(outputColorInfo)
? linearColorInfo
: enableColorTransfers ? linearColorInfo : outputColorInfo;
InputSwitcher inputSwitcher =
new InputSwitcher(
context,
/* outputColorInfo= */ linearColorInfo,
/* outputColorInfo= */ intermediateColorInfo,
glObjectsProvider,
videoFrameProcessingTaskExecutor,
/* errorListenerExecutor= */ videoFrameProcessorListenerExecutor,
@ -729,7 +731,6 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
videoFrameProcessorListenerExecutor,
finalShaderProgramWrapper,
renderFramesAutomatically,
enableColorTransfers,
outputColorInfo);
}
@ -846,10 +847,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
*/
private void configureEffects(InputStreamInfo inputStreamInfo, boolean forceReconfigure)
throws VideoFrameProcessingException {
checkColors(
/* inputColorInfo= */ inputStreamInfo.frameInfo.colorInfo,
outputColorInfo,
enableColorTransfers);
checkColors(/* inputColorInfo= */ inputStreamInfo.frameInfo.colorInfo, outputColorInfo);
if (forceReconfigure || !activeEffects.equals(inputStreamInfo.effects)) {
if (!intermediateGlShaderPrograms.isEmpty()) {
for (int i = 0; i < intermediateGlShaderPrograms.size(); i++) {
@ -886,14 +884,12 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
}
/** Checks that color configuration is valid for {@link DefaultVideoFrameProcessor}. */
private static void checkColors(
ColorInfo inputColorInfo, ColorInfo outputColorInfo, boolean enableColorTransfers)
private static void checkColors(ColorInfo inputColorInfo, ColorInfo outputColorInfo)
throws VideoFrameProcessingException {
if (ColorInfo.isTransferHdr(inputColorInfo)) {
checkArgument(inputColorInfo.colorSpace == C.COLOR_SPACE_BT2020);
}
if ((ColorInfo.isTransferHdr(inputColorInfo) || ColorInfo.isTransferHdr(outputColorInfo))) {
checkArgument(enableColorTransfers);
long glVersion;
try {
glVersion = GlUtil.getContextMajorVersion();

View File

@ -37,6 +37,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.Util;
import androidx.media3.effect.DefaultVideoFrameProcessor;
import androidx.media3.transformer.Composition;
import androidx.media3.transformer.EditedMediaItem;
import androidx.media3.transformer.ExportException;
@ -208,6 +209,39 @@ public final class HdrEditingTest {
assertThat(actualColorTransfer).isEqualTo(C.COLOR_TRANSFER_HLG);
}
@Test
public void exportAndTranscodeHdr_withDisabledColorTransfers_whenHdrEditingIsSupported()
throws Exception {
Context context = ApplicationProvider.getApplicationContext();
Format format = MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT;
assumeDeviceSupportsHdrEditing(testId, format);
assumeFormatsSupported(context, testId, /* inputFormat= */ format, /* outputFormat= */ format);
Transformer transformer =
new Transformer.Builder(context)
.setVideoFrameProcessorFactory(
new DefaultVideoFrameProcessor.Factory.Builder()
.setEnableColorTransfers(false)
.build())
.build();
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_5_SECOND_HLG10)))
.setEffects(FORCE_TRANSCODE_VIDEO_EFFECTS)
.build();
ExportTestResult exportTestResult =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, editedMediaItem);
@C.ColorTransfer
int actualColorTransfer =
retrieveTrackFormat(context, exportTestResult.filePath, C.TRACK_TYPE_VIDEO)
.colorInfo
.colorTransfer;
assertThat(actualColorTransfer).isEqualTo(C.COLOR_TRANSFER_HLG);
}
@Test
public void exportAndTranscode_hdr10File_whenHdrEditingUnsupported_toneMapsOrThrows()
throws Exception {

View File

@ -279,6 +279,46 @@ public final class ToneMapHdrToSdrUsingOpenGlPixelTest {
.isAtMost(MAXIMUM_DEVICE_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
@Test
public void toneMap_withDisabledColorTransfers_matchesGoldenFile() throws Exception {
assumeDeviceSupportsOpenGlToneMapping(testId, HLG_ASSET_FORMAT);
videoFrameProcessorTestRunner =
new VideoFrameProcessorTestRunner.Builder()
.setTestId(testId)
.setVideoFrameProcessorFactory(
new DefaultVideoFrameProcessor.Factory.Builder()
.setEnableColorTransfers(false)
.build())
.setVideoAssetPath(HLG_ASSET_STRING)
.setOutputColorInfo(TONE_MAP_SDR_COLOR)
.build();
Bitmap expectedBitmap = readBitmap(TONE_MAP_HLG_TO_SDR_PNG_ASSET_PATH);
Bitmap actualBitmap;
try {
videoFrameProcessorTestRunner.processFirstFrameAndEnd();
actualBitmap = videoFrameProcessorTestRunner.getOutputBitmap();
} catch (UnsupportedOperationException e) {
if (e.getMessage() != null
&& e.getMessage().equals(DecodeOneFrameUtil.NO_DECODER_SUPPORT_ERROR_STRING)) {
recordTestSkipped(
getApplicationContext(),
testId,
/* reason= */ DecodeOneFrameUtil.NO_DECODER_SUPPORT_ERROR_STRING);
return;
} else {
throw e;
}
}
Log.i(TAG, "Successfully tone mapped.");
// TODO(b/207848601): Switch to using proper tooling for testing against golden data.
float averagePixelAbsoluteDifference =
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
assertThat(averagePixelAbsoluteDifference)
.isAtMost(MAXIMUM_DEVICE_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
}
private static VideoFrameProcessorTestRunner.Builder getDefaultFrameProcessorTestRunnerBuilder(
String testId) {
return new VideoFrameProcessorTestRunner.Builder()