Add support for requesting color transfer to SDR

From Android T onwards `MediaCodec` supports requesting tone-mapping down to
SDR. Add an option to request this behavior and document that it isn't
supported before T. Also add an option in the demo app to try it out.

Tested manually on a prerelease build.

PiperOrigin-RevId: 437765325
This commit is contained in:
andrewlewis 2022-03-28 16:29:56 +01:00 committed by Ian Baker
parent 79db98e733
commit d2a9419ad3
9 changed files with 70 additions and 5 deletions

View File

@ -54,6 +54,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String SCALE_Y = "scale_y"; public static final String SCALE_Y = "scale_y";
public static final String ROTATE_DEGREES = "rotate_degrees"; public static final String ROTATE_DEGREES = "rotate_degrees";
public static final String ENABLE_FALLBACK = "enable_fallback"; public static final String ENABLE_FALLBACK = "enable_fallback";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
private static final String[] INPUT_URIS = { private static final String[] INPUT_URIS = {
"https://html5demos.com/assets/dizzy.mp4", "https://html5demos.com/assets/dizzy.mp4",
@ -84,6 +85,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner scaleSpinner;
private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox enableFallbackCheckBox; private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private int inputUriPosition; private int inputUriPosition;
@ -150,6 +152,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180"); rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180");
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
} }
@ -179,6 +182,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableFallbackCheckBox", "enableFallbackCheckBox",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
}) })
private void startTransformation(View view) { private void startTransformation(View view) {
@ -211,6 +215,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate));
} }
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
transformerIntent.putExtras(bundle); transformerIntent.putExtras(bundle);
@ -243,6 +249,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
}) })
private void onRemoveAudio(View view) { private void onRemoveAudio(View view) {
@ -261,6 +268,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
}) })
private void onRemoveVideo(View view) { private void onRemoveVideo(View view) {
@ -278,6 +286,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox" "enableHdrEditingCheckBox"
}) })
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
@ -286,6 +295,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
resolutionHeightSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled);
enableRequestSdrToneMappingCheckBox.setEnabled(isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
@ -293,6 +303,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled);
findViewById(R.id.rotate).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
} }
} }

View File

@ -225,6 +225,8 @@ public final class TransformerActivity extends AppCompatActivity {
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
requestBuilder.setRotationDegrees(rotateDegrees); requestBuilder.setRotationDegrees(rotateDegrees);
requestBuilder.setEnableRequestSdrToneMapping(
bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING));
requestBuilder.experimental_setEnableHdrEditing( requestBuilder.experimental_setEnableHdrEditing(
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
transformerBuilder transformerBuilder

View File

@ -169,6 +169,16 @@
android:layout_gravity="right" android:layout_gravity="right"
android:checked="true"/> android:checked="true"/>
</TableRow> </TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/request_sdr_tone_mapping"
android:text="@string/request_sdr_tone_mapping" />
<CheckBox
android:id="@+id/request_sdr_tone_mapping_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow <TableRow
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" > android:gravity="center_vertical" >

View File

@ -27,8 +27,9 @@
<string name="scale" translatable="false">Scale video</string> <string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string> <string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string> <string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="transform" translatable="false">Transform</string> <string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string> <string name="hdr_editing" translatable="false">[Experimental] HDR editing</string>
<string name="transform" translatable="false">Transform</string>
<string name="debug_preview" translatable="false">Debug preview:</string> <string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string> <string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
<string name="transformation_started" translatable="false">Transformation started</string> <string name="transformation_started" translatable="false">Transformation started</string>

View File

@ -58,10 +58,12 @@ public interface Codec {
* @param format The {@link Format} (of the input data) used to determine the underlying decoder * @param format The {@link Format} (of the input data) used to determine the underlying decoder
* and its configuration values. * and its configuration values.
* @param outputSurface The {@link Surface} to which the decoder output is rendered. * @param outputSurface The {@link Surface} to which the decoder output is rendered.
* @param enableRequestSdrToneMapping Whether to request tone-mapping to SDR.
* @return A {@link Codec} for video decoding. * @return A {@link Codec} for video decoding.
* @throws TransformationException If no suitable {@link Codec} can be created. * @throws TransformationException If no suitable {@link Codec} can be created.
*/ */
Codec createForVideoDecoding(Format format, Surface outputSurface) Codec createForVideoDecoding(
Format format, Surface outputSurface, boolean enableRequestSdrToneMapping)
throws TransformationException; throws TransformationException;
} }

View File

@ -49,7 +49,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} }
@Override @Override
public Codec createForVideoDecoding(Format format, Surface outputSurface) public Codec createForVideoDecoding(
Format format, Surface outputSurface, boolean enableRequestSdrToneMapping)
throws TransformationException { throws TransformationException {
MediaFormat mediaFormat = MediaFormat mediaFormat =
MediaFormat.createVideoFormat( MediaFormat.createVideoFormat(
@ -63,6 +64,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
// cycle. This key ensures no frame dropping when the decoder's output surface is full. // cycle. This key ensures no frame dropping when the decoder's output surface is full.
mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0);
} }
if (SDK_INT >= 31 && enableRequestSdrToneMapping) {
mediaFormat.setInteger(
MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO);
}
@Nullable @Nullable
String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true); String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true);

View File

@ -40,6 +40,7 @@ public final class TransformationRequest {
private int outputHeight; private int outputHeight;
@Nullable private String audioMimeType; @Nullable private String audioMimeType;
@Nullable private String videoMimeType; @Nullable private String videoMimeType;
private boolean enableRequestSdrToneMapping;
private boolean enableHdrEditing; private boolean enableHdrEditing;
/** /**
@ -62,6 +63,7 @@ public final class TransformationRequest {
this.outputHeight = transformationRequest.outputHeight; this.outputHeight = transformationRequest.outputHeight;
this.audioMimeType = transformationRequest.audioMimeType; this.audioMimeType = transformationRequest.audioMimeType;
this.videoMimeType = transformationRequest.videoMimeType; this.videoMimeType = transformationRequest.videoMimeType;
this.enableRequestSdrToneMapping = transformationRequest.enableRequestSdrToneMapping;
this.enableHdrEditing = transformationRequest.enableHdrEditing; this.enableHdrEditing = transformationRequest.enableHdrEditing;
} }
@ -194,13 +196,30 @@ public final class TransformationRequest {
return this; return this;
} }
/**
* Sets whether to request tone-mapping to standard dynamic range (SDR). If enabled and
* supported, high dynamic range (HDR) input will be tone-mapped into an SDR opto-electrical
* transfer function before processing.
*
* <p>The setting has no effect if the input is already in SDR, or if tone-mapping is not
* supported. Currently tone-mapping is only guaranteed to be supported from Android T onwards.
*
* @param enableRequestSdrToneMapping Whether to request tone-mapping down to SDR.
* @return This builder.
*/
public Builder setEnableRequestSdrToneMapping(boolean enableRequestSdrToneMapping) {
this.enableRequestSdrToneMapping = enableRequestSdrToneMapping;
return this;
}
/** /**
* Sets whether to attempt to process any input video stream as a high dynamic range (HDR) * Sets whether to attempt to process any input video stream as a high dynamic range (HDR)
* signal. * signal.
* *
* <p>This method is experimental, and will be renamed or removed in a future release. The HDR * <p>This method is experimental, and will be renamed or removed in a future release. The HDR
* editing feature is under development and is intended for developing/testing HDR processing * editing feature is under development and is intended for developing/testing HDR processing
* and encoding support. * and encoding support. HDR editing can't be enabled at the same time as {@link
* #setEnableRequestSdrToneMapping(boolean) SDR tone-mapping}.
* *
* @param enableHdrEditing Whether to attempt to process any input video stream as a high * @param enableHdrEditing Whether to attempt to process any input video stream as a high
* dynamic range (HDR) signal. * dynamic range (HDR) signal.
@ -221,6 +240,7 @@ public final class TransformationRequest {
outputHeight, outputHeight,
audioMimeType, audioMimeType,
videoMimeType, videoMimeType,
enableRequestSdrToneMapping,
enableHdrEditing); enableHdrEditing);
} }
} }
@ -271,6 +291,9 @@ public final class TransformationRequest {
* @see Builder#setVideoMimeType(String) * @see Builder#setVideoMimeType(String)
*/ */
@Nullable public final String videoMimeType; @Nullable public final String videoMimeType;
/** Whether to request tone-mapping to standard dynamic range (SDR). */
public final boolean enableRequestSdrToneMapping;
/** /**
* Whether to attempt to process any input video stream as a high dynamic range (HDR) signal. * Whether to attempt to process any input video stream as a high dynamic range (HDR) signal.
* *
@ -286,7 +309,9 @@ public final class TransformationRequest {
int outputHeight, int outputHeight,
@Nullable String audioMimeType, @Nullable String audioMimeType,
@Nullable String videoMimeType, @Nullable String videoMimeType,
boolean enableRequestSdrToneMapping,
boolean enableHdrEditing) { boolean enableHdrEditing) {
checkArgument(!enableHdrEditing || !enableRequestSdrToneMapping);
this.flattenForSlowMotion = flattenForSlowMotion; this.flattenForSlowMotion = flattenForSlowMotion;
this.scaleX = scaleX; this.scaleX = scaleX;
this.scaleY = scaleY; this.scaleY = scaleY;
@ -294,6 +319,7 @@ public final class TransformationRequest {
this.outputHeight = outputHeight; this.outputHeight = outputHeight;
this.audioMimeType = audioMimeType; this.audioMimeType = audioMimeType;
this.videoMimeType = videoMimeType; this.videoMimeType = videoMimeType;
this.enableRequestSdrToneMapping = enableRequestSdrToneMapping;
this.enableHdrEditing = enableHdrEditing; this.enableHdrEditing = enableHdrEditing;
} }
@ -313,6 +339,7 @@ public final class TransformationRequest {
&& outputHeight == that.outputHeight && outputHeight == that.outputHeight
&& Util.areEqual(audioMimeType, that.audioMimeType) && Util.areEqual(audioMimeType, that.audioMimeType)
&& Util.areEqual(videoMimeType, that.videoMimeType) && Util.areEqual(videoMimeType, that.videoMimeType)
&& enableRequestSdrToneMapping == that.enableRequestSdrToneMapping
&& enableHdrEditing == that.enableHdrEditing; && enableHdrEditing == that.enableHdrEditing;
} }
@ -325,6 +352,7 @@ public final class TransformationRequest {
result = 31 * result + outputHeight; result = 31 * result + outputHeight;
result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0); result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0);
result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0); result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0);
result = 31 * result + (enableRequestSdrToneMapping ? 1 : 0);
result = 31 * result + (enableHdrEditing ? 1 : 0); result = 31 * result + (enableHdrEditing ? 1 : 0);
return result; return result;
} }

View File

@ -107,6 +107,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
if (encoderFactory.videoNeedsEncoding()) { if (encoderFactory.videoNeedsEncoding()) {
return false; return false;
} }
if (transformationRequest.enableRequestSdrToneMapping) {
return false;
}
if (transformationRequest.enableHdrEditing) { if (transformationRequest.enableHdrEditing) {
return false; return false;
} }

View File

@ -124,7 +124,10 @@ import org.checkerframework.dataflow.qual.Pure;
encoderSupportedFormat.width, encoderSupportedFormat.height)); encoderSupportedFormat.width, encoderSupportedFormat.height));
decoder = decoder =
decoderFactory.createForVideoDecoding(inputFormat, frameProcessorChain.getInputSurface()); decoderFactory.createForVideoDecoding(
inputFormat,
frameProcessorChain.getInputSurface(),
transformationRequest.enableRequestSdrToneMapping);
maxPendingFrameCount = getMaxPendingFrameCount(); maxPendingFrameCount = getMaxPendingFrameCount();
} }