Transformer GL: Add setResolution() API.
Simple, initial implementation to allow setResolution() to set the output height, for downscaling/upscaling. Per TODOs, follow-up CLs may change layering, add UI, or allow querying decoders for more resolution options. PiperOrigin-RevId: 410203343
This commit is contained in:
parent
1618e0ef8e
commit
79f03bb135
@ -29,12 +29,14 @@ import android.opengl.EGLSurface;
|
|||||||
import android.opengl.GLES20;
|
import android.opengl.GLES20;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.media3.common.Format;
|
|
||||||
import androidx.media3.common.util.GlUtil;
|
import androidx.media3.common.util.GlUtil;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/** Applies OpenGL transformations to video frames. */
|
/**
|
||||||
|
* OpenGlFrameEditor applies changes to individual video frames using OpenGL. Changes include just
|
||||||
|
* resolution for now, but may later include brightness, cropping, rotation, etc.
|
||||||
|
*/
|
||||||
@RequiresApi(18)
|
@RequiresApi(18)
|
||||||
/* package */ final class OpenGlFrameEditor {
|
/* package */ final class OpenGlFrameEditor {
|
||||||
|
|
||||||
@ -42,8 +44,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
GlUtil.glAssertionsEnabled = true;
|
GlUtil.glAssertionsEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new OpenGlFrameEditor for applying changes to individual frames.
|
||||||
|
*
|
||||||
|
* @param context A {@link Context}.
|
||||||
|
* @param outputWidth The output width in pixels.
|
||||||
|
* @param outputHeight The output height in pixels.
|
||||||
|
* @param outputSurface The {@link Surface}.
|
||||||
|
* @return A configured OpenGlFrameEditor.
|
||||||
|
*/
|
||||||
public static OpenGlFrameEditor create(
|
public static OpenGlFrameEditor create(
|
||||||
Context context, Format inputFormat, Surface outputSurface) {
|
Context context, int outputWidth, int outputHeight, Surface outputSurface) {
|
||||||
EGLDisplay eglDisplay = GlUtil.createEglDisplay();
|
EGLDisplay eglDisplay = GlUtil.createEglDisplay();
|
||||||
EGLContext eglContext;
|
EGLContext eglContext;
|
||||||
try {
|
try {
|
||||||
@ -52,7 +63,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
throw new IllegalStateException("EGL version is unsupported", e);
|
throw new IllegalStateException("EGL version is unsupported", e);
|
||||||
}
|
}
|
||||||
EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface);
|
EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface);
|
||||||
GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, inputFormat.width, inputFormat.height);
|
GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight);
|
||||||
int textureId = GlUtil.createExternalTexture();
|
int textureId = GlUtil.createExternalTexture();
|
||||||
GlUtil.Program copyProgram;
|
GlUtil.Program copyProgram;
|
||||||
try {
|
try {
|
||||||
|
@ -71,9 +71,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* <p>Temporary copy of the {@link Transformer} class, which transforms by transcoding rather than
|
* <p>Temporary copy of the {@link Transformer} class, which transforms by transcoding rather than
|
||||||
* by muxing. This class is intended to replace the Transformer class.
|
* by muxing. This class is intended to replace the Transformer class.
|
||||||
*
|
*
|
||||||
* <p>TODO(http://b/202131097): Replace the Transformer class with TranscodingTransformer, and
|
|
||||||
* rename this class to Transformer.
|
|
||||||
*
|
|
||||||
* <p>The same TranscodingTransformer instance can be used to transform multiple inputs
|
* <p>The same TranscodingTransformer instance can be used to transform multiple inputs
|
||||||
* (sequentially, not concurrently).
|
* (sequentially, not concurrently).
|
||||||
*
|
*
|
||||||
@ -89,16 +86,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
@RequiresApi(18)
|
@RequiresApi(18)
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class TranscodingTransformer {
|
public final class TranscodingTransformer {
|
||||||
|
// TODO(http://b/202131097): Replace the Transformer class with TranscodingTransformer, and
|
||||||
|
// rename this class to Transformer.
|
||||||
|
|
||||||
/** A builder for {@link TranscodingTransformer} instances. */
|
/** A builder for {@link TranscodingTransformer} instances. */
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
|
|
||||||
|
// Mandatory field.
|
||||||
private @MonotonicNonNull Context context;
|
private @MonotonicNonNull Context context;
|
||||||
|
|
||||||
|
// Optional fields.
|
||||||
private @MonotonicNonNull MediaSourceFactory mediaSourceFactory;
|
private @MonotonicNonNull MediaSourceFactory mediaSourceFactory;
|
||||||
private Muxer.Factory muxerFactory;
|
private Muxer.Factory muxerFactory;
|
||||||
private boolean removeAudio;
|
private boolean removeAudio;
|
||||||
private boolean removeVideo;
|
private boolean removeVideo;
|
||||||
private boolean flattenForSlowMotion;
|
private boolean flattenForSlowMotion;
|
||||||
|
private int outputHeight;
|
||||||
private String outputMimeType;
|
private String outputMimeType;
|
||||||
@Nullable private String audioMimeType;
|
@Nullable private String audioMimeType;
|
||||||
@Nullable private String videoMimeType;
|
@Nullable private String videoMimeType;
|
||||||
@ -109,6 +112,7 @@ public final class TranscodingTransformer {
|
|||||||
/** Creates a builder with default values. */
|
/** Creates a builder with default values. */
|
||||||
public Builder() {
|
public Builder() {
|
||||||
muxerFactory = new FrameworkMuxer.Factory();
|
muxerFactory = new FrameworkMuxer.Factory();
|
||||||
|
outputHeight = Transformation.NO_VALUE;
|
||||||
outputMimeType = MimeTypes.VIDEO_MP4;
|
outputMimeType = MimeTypes.VIDEO_MP4;
|
||||||
listener = new Listener() {};
|
listener = new Listener() {};
|
||||||
looper = Util.getCurrentOrMainLooper();
|
looper = Util.getCurrentOrMainLooper();
|
||||||
@ -123,6 +127,7 @@ public final class TranscodingTransformer {
|
|||||||
this.removeAudio = transcodingTransformer.transformation.removeAudio;
|
this.removeAudio = transcodingTransformer.transformation.removeAudio;
|
||||||
this.removeVideo = transcodingTransformer.transformation.removeVideo;
|
this.removeVideo = transcodingTransformer.transformation.removeVideo;
|
||||||
this.flattenForSlowMotion = transcodingTransformer.transformation.flattenForSlowMotion;
|
this.flattenForSlowMotion = transcodingTransformer.transformation.flattenForSlowMotion;
|
||||||
|
this.outputHeight = transcodingTransformer.transformation.outputHeight;
|
||||||
this.outputMimeType = transcodingTransformer.transformation.outputMimeType;
|
this.outputMimeType = transcodingTransformer.transformation.outputMimeType;
|
||||||
this.audioMimeType = transcodingTransformer.transformation.audioMimeType;
|
this.audioMimeType = transcodingTransformer.transformation.audioMimeType;
|
||||||
this.videoMimeType = transcodingTransformer.transformation.videoMimeType;
|
this.videoMimeType = transcodingTransformer.transformation.videoMimeType;
|
||||||
@ -215,6 +220,37 @@ public final class TranscodingTransformer {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the output resolution using the output height. The default value is {@link
|
||||||
|
* Transformation#NO_VALUE}, which will use the same height as the input. Output width will
|
||||||
|
* scale to preserve the input video's aspect ratio.
|
||||||
|
*
|
||||||
|
* <p>For now, only "popular" heights like 240, 360, 480, 720, 1080, 1440, or 2160 are
|
||||||
|
* supported, to ensure compatibility on different devices.
|
||||||
|
*
|
||||||
|
* <p>For example, a 1920x1440 video can be scaled to 640x480 by calling setResolution(480).
|
||||||
|
*
|
||||||
|
* @param outputHeight The output height in pixels.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
public Builder setResolution(int outputHeight) {
|
||||||
|
// TODO(Internal b/201293185): Restructure to input a Presentation class.
|
||||||
|
// TODO(Internal b/201293185): Check encoder codec capabilities in order to allow arbitrary
|
||||||
|
// resolutions and reasonable fallbacks.
|
||||||
|
if (outputHeight != 240
|
||||||
|
&& outputHeight != 360
|
||||||
|
&& outputHeight != 480
|
||||||
|
&& outputHeight != 720
|
||||||
|
&& outputHeight != 1080
|
||||||
|
&& outputHeight != 1440
|
||||||
|
&& outputHeight != 2160) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Please use a height of 240, 360, 480, 720, 1080, 1440, or 2160.");
|
||||||
|
}
|
||||||
|
this.outputHeight = outputHeight;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the MIME type of the output. The default value is {@link MimeTypes#VIDEO_MP4}. Supported
|
* Sets the MIME type of the output. The default value is {@link MimeTypes#VIDEO_MP4}. Supported
|
||||||
* values are:
|
* values are:
|
||||||
@ -369,6 +405,7 @@ public final class TranscodingTransformer {
|
|||||||
removeAudio,
|
removeAudio,
|
||||||
removeVideo,
|
removeVideo,
|
||||||
flattenForSlowMotion,
|
flattenForSlowMotion,
|
||||||
|
outputHeight,
|
||||||
outputMimeType,
|
outputMimeType,
|
||||||
audioMimeType,
|
audioMimeType,
|
||||||
videoMimeType);
|
videoMimeType);
|
||||||
|
@ -21,9 +21,13 @@ import androidx.annotation.Nullable;
|
|||||||
/** A media transformation configuration. */
|
/** A media transformation configuration. */
|
||||||
/* package */ final class Transformation {
|
/* package */ final class Transformation {
|
||||||
|
|
||||||
|
/** A value for various fields to indicate that the field's value is unknown or not set. */
|
||||||
|
public static final int NO_VALUE = -1;
|
||||||
|
|
||||||
public final boolean removeAudio;
|
public final boolean removeAudio;
|
||||||
public final boolean removeVideo;
|
public final boolean removeVideo;
|
||||||
public final boolean flattenForSlowMotion;
|
public final boolean flattenForSlowMotion;
|
||||||
|
public final int outputHeight;
|
||||||
public final String outputMimeType;
|
public final String outputMimeType;
|
||||||
@Nullable public final String audioMimeType;
|
@Nullable public final String audioMimeType;
|
||||||
@Nullable public final String videoMimeType;
|
@Nullable public final String videoMimeType;
|
||||||
@ -32,12 +36,14 @@ import androidx.annotation.Nullable;
|
|||||||
boolean removeAudio,
|
boolean removeAudio,
|
||||||
boolean removeVideo,
|
boolean removeVideo,
|
||||||
boolean flattenForSlowMotion,
|
boolean flattenForSlowMotion,
|
||||||
|
int outputHeight,
|
||||||
String outputMimeType,
|
String outputMimeType,
|
||||||
@Nullable String audioMimeType,
|
@Nullable String audioMimeType,
|
||||||
@Nullable String videoMimeType) {
|
@Nullable String videoMimeType) {
|
||||||
this.removeAudio = removeAudio;
|
this.removeAudio = removeAudio;
|
||||||
this.removeVideo = removeVideo;
|
this.removeVideo = removeVideo;
|
||||||
this.flattenForSlowMotion = flattenForSlowMotion;
|
this.flattenForSlowMotion = flattenForSlowMotion;
|
||||||
|
this.outputHeight = outputHeight;
|
||||||
this.outputMimeType = outputMimeType;
|
this.outputMimeType = outputMimeType;
|
||||||
this.audioMimeType = audioMimeType;
|
this.audioMimeType = audioMimeType;
|
||||||
this.videoMimeType = videoMimeType;
|
this.videoMimeType = videoMimeType;
|
||||||
|
@ -304,6 +304,7 @@ public final class Transformer {
|
|||||||
removeAudio,
|
removeAudio,
|
||||||
removeVideo,
|
removeVideo,
|
||||||
flattenForSlowMotion,
|
flattenForSlowMotion,
|
||||||
|
/* outputHeight= */ Transformation.NO_VALUE,
|
||||||
outputMimeType,
|
outputMimeType,
|
||||||
/* audioMimeType= */ null,
|
/* audioMimeType= */ null,
|
||||||
/* videoMimeType= */ null);
|
/* videoMimeType= */ null);
|
||||||
|
@ -83,7 +83,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {}
|
while (feedMuxerFromPipeline() || samplePipeline.processData() || feedPipelineFromInput()) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attempts to read the input format and to initialize the sample pipeline. */
|
/** Attempts to read the input format and to initialize the sample or passthrough pipeline. */
|
||||||
@EnsuresNonNullIf(expression = "samplePipeline", result = true)
|
@EnsuresNonNullIf(expression = "samplePipeline", result = true)
|
||||||
private boolean ensureRendererConfigured() throws ExoPlaybackException {
|
private boolean ensureRendererConfigured() throws ExoPlaybackException {
|
||||||
if (samplePipeline != null) {
|
if (samplePipeline != null) {
|
||||||
@ -96,8 +96,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Format decoderInputFormat = checkNotNull(formatHolder.format);
|
Format decoderInputFormat = checkNotNull(formatHolder.format);
|
||||||
if (transformation.videoMimeType != null
|
if ((transformation.videoMimeType != null
|
||||||
&& !transformation.videoMimeType.equals(decoderInputFormat.sampleMimeType)) {
|
&& !transformation.videoMimeType.equals(decoderInputFormat.sampleMimeType))
|
||||||
|
|| (transformation.outputHeight != Transformation.NO_VALUE
|
||||||
|
&& transformation.outputHeight != decoderInputFormat.height)) {
|
||||||
samplePipeline =
|
samplePipeline =
|
||||||
new VideoSamplePipeline(context, decoderInputFormat, transformation, getIndex());
|
new VideoSamplePipeline(context, decoderInputFormat, transformation, getIndex());
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,12 +57,22 @@ import java.io.IOException;
|
|||||||
|
|
||||||
encoderOutputBuffer =
|
encoderOutputBuffer =
|
||||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||||
|
|
||||||
|
int outputWidth = decoderInputFormat.width;
|
||||||
|
int outputHeight = decoderInputFormat.height;
|
||||||
|
if (transformation.outputHeight != Transformation.NO_VALUE
|
||||||
|
&& transformation.outputHeight != decoderInputFormat.height) {
|
||||||
|
outputWidth =
|
||||||
|
decoderInputFormat.width * transformation.outputHeight / decoderInputFormat.height;
|
||||||
|
outputHeight = transformation.outputHeight;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
encoder =
|
encoder =
|
||||||
MediaCodecAdapterWrapper.createForVideoEncoding(
|
MediaCodecAdapterWrapper.createForVideoEncoding(
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
.setWidth(decoderInputFormat.width)
|
.setWidth(outputWidth)
|
||||||
.setHeight(decoderInputFormat.height)
|
.setHeight(outputHeight)
|
||||||
.setSampleMimeType(
|
.setSampleMimeType(
|
||||||
transformation.videoMimeType != null
|
transformation.videoMimeType != null
|
||||||
? transformation.videoMimeType
|
? transformation.videoMimeType
|
||||||
@ -77,7 +87,8 @@ import java.io.IOException;
|
|||||||
openGlFrameEditor =
|
openGlFrameEditor =
|
||||||
OpenGlFrameEditor.create(
|
OpenGlFrameEditor.create(
|
||||||
context,
|
context,
|
||||||
decoderInputFormat,
|
outputWidth,
|
||||||
|
outputHeight,
|
||||||
/* outputSurface= */ checkNotNull(encoder.getInputSurface()));
|
/* outputSurface= */ checkNotNull(encoder.getInputSurface()));
|
||||||
try {
|
try {
|
||||||
decoder =
|
decoder =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user