Work around SurfaceTexture implicit scale
If MediaCodec allocates passes an image buffer with a cropped region, SurfaceTexture.getTransformMatrix will cut off 2 pixels from each dimensions. The resulting videos will appear a little stretched. This patch inspects the SurfaceTexture transform matrix, and guesses what the unscaled transform matrix should be. Behind experimentalAdjustSurfaceTextureTransformationMatrix flag PiperOrigin-RevId: 635721267
@ -46,6 +46,8 @@ public final class GlProgram {
|
|||||||
private final Map<String, Attribute> attributeByName;
|
private final Map<String, Attribute> attributeByName;
|
||||||
private final Map<String, Uniform> uniformByName;
|
private final Map<String, Uniform> uniformByName;
|
||||||
|
|
||||||
|
private boolean externalTexturesRequireNearestSampling;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
|
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
|
||||||
*
|
*
|
||||||
@ -219,10 +221,20 @@ public final class GlProgram {
|
|||||||
attribute.bind();
|
attribute.bind();
|
||||||
}
|
}
|
||||||
for (Uniform uniform : uniforms) {
|
for (Uniform uniform : uniforms) {
|
||||||
uniform.bind();
|
uniform.bind(externalTexturesRequireNearestSampling);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to sample external textures with GL_NEAREST.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@code false}.
|
||||||
|
*/
|
||||||
|
public void setExternalTexturesRequireNearestSampling(
|
||||||
|
boolean externalTexturesRequireNearestSampling) {
|
||||||
|
this.externalTexturesRequireNearestSampling = externalTexturesRequireNearestSampling;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the length of the null-terminated C string in {@code cString}. */
|
/** Returns the length of the null-terminated C string in {@code cString}. */
|
||||||
private static int getCStringLength(byte[] cString) {
|
private static int getCStringLength(byte[] cString) {
|
||||||
for (int i = 0; i < cString.length; ++i) {
|
for (int i = 0; i < cString.length; ++i) {
|
||||||
@ -363,7 +375,8 @@ public final class GlProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
|
* Configures {@link #bind(boolean)} to use the specified {@code texId} for this sampler
|
||||||
|
* uniform.
|
||||||
*
|
*
|
||||||
* @param texId The GL texture identifier from which to sample.
|
* @param texId The GL texture identifier from which to sample.
|
||||||
* @param texUnitIndex The GL texture unit index.
|
* @param texUnitIndex The GL texture unit index.
|
||||||
@ -373,22 +386,22 @@ public final class GlProgram {
|
|||||||
this.texUnitIndex = texUnitIndex;
|
this.texUnitIndex = texUnitIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configures {@link #bind()} to use the specified {@code int} {@code value}. */
|
/** Configures {@link #bind(boolean)} to use the specified {@code int} {@code value}. */
|
||||||
public void setInt(int value) {
|
public void setInt(int value) {
|
||||||
this.intValue[0] = value;
|
this.intValue[0] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configures {@link #bind()} to use the specified {@code int[]} {@code value}. */
|
/** Configures {@link #bind(boolean)} to use the specified {@code int[]} {@code value}. */
|
||||||
public void setInts(int[] value) {
|
public void setInts(int[] value) {
|
||||||
System.arraycopy(value, /* srcPos= */ 0, this.intValue, /* destPos= */ 0, value.length);
|
System.arraycopy(value, /* srcPos= */ 0, this.intValue, /* destPos= */ 0, value.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configures {@link #bind()} to use the specified {@code float} {@code value}. */
|
/** Configures {@link #bind(boolean)} to use the specified {@code float} {@code value}. */
|
||||||
public void setFloat(float value) {
|
public void setFloat(float value) {
|
||||||
this.floatValue[0] = value;
|
this.floatValue[0] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Configures {@link #bind()} to use the specified {@code float[]} {@code value}. */
|
/** Configures {@link #bind(boolean)} to use the specified {@code float[]} {@code value}. */
|
||||||
public void setFloats(float[] value) {
|
public void setFloats(float[] value) {
|
||||||
System.arraycopy(value, /* srcPos= */ 0, this.floatValue, /* destPos= */ 0, value.length);
|
System.arraycopy(value, /* srcPos= */ 0, this.floatValue, /* destPos= */ 0, value.length);
|
||||||
}
|
}
|
||||||
@ -398,8 +411,12 @@ public final class GlProgram {
|
|||||||
* #setFloat(float)} or {@link #setFloats(float[])}.
|
* #setFloat(float)} or {@link #setFloats(float[])}.
|
||||||
*
|
*
|
||||||
* <p>Should be called before each drawing call.
|
* <p>Should be called before each drawing call.
|
||||||
|
*
|
||||||
|
* @param externalTexturesRequireNearestSampling Whether the external texture requires
|
||||||
|
* GL_NEAREST sampling to avoid sampling from undefined region, which could happen when
|
||||||
|
* using GL_LINEAR.
|
||||||
*/
|
*/
|
||||||
public void bind() throws GlUtil.GlException {
|
public void bind(boolean externalTexturesRequireNearestSampling) throws GlUtil.GlException {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case GLES20.GL_INT:
|
case GLES20.GL_INT:
|
||||||
GLES20.glUniform1iv(location, /* count= */ 1, intValue, /* offset= */ 0);
|
GLES20.glUniform1iv(location, /* count= */ 1, intValue, /* offset= */ 0);
|
||||||
@ -455,7 +472,10 @@ public final class GlProgram {
|
|||||||
type == GLES20.GL_SAMPLER_2D
|
type == GLES20.GL_SAMPLER_2D
|
||||||
? GLES20.GL_TEXTURE_2D
|
? GLES20.GL_TEXTURE_2D
|
||||||
: GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
: GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
||||||
texIdValue);
|
texIdValue,
|
||||||
|
type == GLES20.GL_SAMPLER_2D && !externalTexturesRequireNearestSampling
|
||||||
|
? GLES20.GL_LINEAR
|
||||||
|
: GLES20.GL_NEAREST);
|
||||||
GLES20.glUniform1i(location, texUnitIndex);
|
GLES20.glUniform1i(location, texUnitIndex);
|
||||||
GlUtil.checkGlError();
|
GlUtil.checkGlError();
|
||||||
break;
|
break;
|
||||||
|
@ -632,7 +632,7 @@ public final class GlUtil {
|
|||||||
*/
|
*/
|
||||||
public static int createExternalTexture() throws GlException {
|
public static int createExternalTexture() throws GlException {
|
||||||
int texId = generateTexture();
|
int texId = generateTexture();
|
||||||
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
|
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId, GLES20.GL_LINEAR);
|
||||||
return texId;
|
return texId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,7 +687,7 @@ public final class GlUtil {
|
|||||||
throws GlException {
|
throws GlException {
|
||||||
assertValidTextureSize(width, height);
|
assertValidTextureSize(width, height);
|
||||||
int texId = generateTexture();
|
int texId = generateTexture();
|
||||||
bindTexture(GLES20.GL_TEXTURE_2D, texId);
|
bindTexture(GLES20.GL_TEXTURE_2D, texId, GLES20.GL_LINEAR);
|
||||||
GLES20.glTexImage2D(
|
GLES20.glTexImage2D(
|
||||||
GLES20.GL_TEXTURE_2D,
|
GLES20.GL_TEXTURE_2D,
|
||||||
/* level= */ 0,
|
/* level= */ 0,
|
||||||
@ -713,26 +713,29 @@ public final class GlUtil {
|
|||||||
/** Sets the {@code texId} to contain the {@link Bitmap bitmap} data and size. */
|
/** Sets the {@code texId} to contain the {@link Bitmap bitmap} data and size. */
|
||||||
public static void setTexture(int texId, Bitmap bitmap) throws GlException {
|
public static void setTexture(int texId, Bitmap bitmap) throws GlException {
|
||||||
assertValidTextureSize(bitmap.getWidth(), bitmap.getHeight());
|
assertValidTextureSize(bitmap.getWidth(), bitmap.getHeight());
|
||||||
bindTexture(GLES20.GL_TEXTURE_2D, texId);
|
bindTexture(GLES20.GL_TEXTURE_2D, texId, GLES20.GL_LINEAR);
|
||||||
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0);
|
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0);
|
||||||
checkGlError();
|
checkGlError();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds the texture of the given type with default configuration of GL_LINEAR filtering and
|
* Binds the texture of the given type with the specified MIN and MAG sampling filter and
|
||||||
* GL_CLAMP_TO_EDGE wrapping.
|
* GL_CLAMP_TO_EDGE wrapping.
|
||||||
*
|
*
|
||||||
* @param textureTarget The target to which the texture is bound, e.g. {@link
|
* @param textureTarget The target to which the texture is bound, e.g. {@link
|
||||||
* GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
|
* GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
|
||||||
* GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
|
* GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
|
||||||
* @param texId The texture identifier.
|
* @param texId The texture identifier.
|
||||||
|
* @param sampleFilter The texture sample filter for both {@link GLES20#GL_TEXTURE_MAG_FILTER} and
|
||||||
|
* {@link GLES20#GL_TEXTURE_MIN_FILTER}.
|
||||||
*/
|
*/
|
||||||
public static void bindTexture(int textureTarget, int texId) throws GlException {
|
public static void bindTexture(int textureTarget, int texId, int sampleFilter)
|
||||||
|
throws GlException {
|
||||||
GLES20.glBindTexture(textureTarget, texId);
|
GLES20.glBindTexture(textureTarget, texId);
|
||||||
checkGlError();
|
checkGlError();
|
||||||
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
|
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, sampleFilter);
|
||||||
checkGlError();
|
checkGlError();
|
||||||
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
|
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, sampleFilter);
|
||||||
checkGlError();
|
checkGlError();
|
||||||
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
|
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
|
||||||
checkGlError();
|
checkGlError();
|
||||||
|
@ -68,6 +68,7 @@ public final class DebugTraceUtil {
|
|||||||
EVENT_OUTPUT_ENDED,
|
EVENT_OUTPUT_ENDED,
|
||||||
EVENT_REGISTER_NEW_INPUT_STREAM,
|
EVENT_REGISTER_NEW_INPUT_STREAM,
|
||||||
EVENT_SURFACE_TEXTURE_INPUT,
|
EVENT_SURFACE_TEXTURE_INPUT,
|
||||||
|
EVENT_SURFACE_TEXTURE_TRANSFORM_FIX,
|
||||||
EVENT_QUEUE_FRAME,
|
EVENT_QUEUE_FRAME,
|
||||||
EVENT_QUEUE_BITMAP,
|
EVENT_QUEUE_BITMAP,
|
||||||
EVENT_QUEUE_TEXTURE,
|
EVENT_QUEUE_TEXTURE,
|
||||||
@ -95,6 +96,7 @@ public final class DebugTraceUtil {
|
|||||||
public static final String EVENT_OUTPUT_ENDED = "OutputEnded";
|
public static final String EVENT_OUTPUT_ENDED = "OutputEnded";
|
||||||
public static final String EVENT_REGISTER_NEW_INPUT_STREAM = "RegisterNewInputStream";
|
public static final String EVENT_REGISTER_NEW_INPUT_STREAM = "RegisterNewInputStream";
|
||||||
public static final String EVENT_SURFACE_TEXTURE_INPUT = "SurfaceTextureInput";
|
public static final String EVENT_SURFACE_TEXTURE_INPUT = "SurfaceTextureInput";
|
||||||
|
public static final String EVENT_SURFACE_TEXTURE_TRANSFORM_FIX = "SurfaceTextureTransformFix";
|
||||||
public static final String EVENT_QUEUE_FRAME = "QueueFrame";
|
public static final String EVENT_QUEUE_FRAME = "QueueFrame";
|
||||||
public static final String EVENT_QUEUE_BITMAP = "QueueBitmap";
|
public static final String EVENT_QUEUE_BITMAP = "QueueBitmap";
|
||||||
public static final String EVENT_QUEUE_TEXTURE = "QueueTexture";
|
public static final String EVENT_QUEUE_TEXTURE = "QueueTexture";
|
||||||
@ -196,7 +198,9 @@ public final class DebugTraceUtil {
|
|||||||
EVENT_OUTPUT_TEXTURE_RENDERED,
|
EVENT_OUTPUT_TEXTURE_RENDERED,
|
||||||
EVENT_RECEIVE_END_OF_ALL_INPUT,
|
EVENT_RECEIVE_END_OF_ALL_INPUT,
|
||||||
EVENT_SIGNAL_ENDED))
|
EVENT_SIGNAL_ENDED))
|
||||||
.put(COMPONENT_EXTERNAL_TEXTURE_MANAGER, ImmutableList.of(EVENT_SIGNAL_EOS))
|
.put(
|
||||||
|
COMPONENT_EXTERNAL_TEXTURE_MANAGER,
|
||||||
|
ImmutableList.of(EVENT_SIGNAL_EOS, EVENT_SURFACE_TEXTURE_TRANSFORM_FIX))
|
||||||
.put(COMPONENT_BITMAP_TEXTURE_MANAGER, ImmutableList.of(EVENT_SIGNAL_EOS))
|
.put(COMPONENT_BITMAP_TEXTURE_MANAGER, ImmutableList.of(EVENT_SIGNAL_EOS))
|
||||||
.put(COMPONENT_TEX_ID_TEXTURE_MANAGER, ImmutableList.of(EVENT_SIGNAL_EOS))
|
.put(COMPONENT_TEX_ID_TEXTURE_MANAGER, ImmutableList.of(EVENT_SIGNAL_EOS))
|
||||||
.put(COMPONENT_COMPOSITOR, ImmutableList.of(EVENT_OUTPUT_TEXTURE_RENDERED))
|
.put(COMPONENT_COMPOSITOR, ImmutableList.of(EVENT_OUTPUT_TEXTURE_RENDERED))
|
||||||
|
@ -269,6 +269,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* If this is an optical color, it must be BT.2020 if {@code inputColorInfo} is {@linkplain
|
* If this is an optical color, it must be BT.2020 if {@code inputColorInfo} is {@linkplain
|
||||||
* ColorInfo#isTransferHdr(ColorInfo) HDR}, and RGB BT.709 if not.
|
* ColorInfo#isTransferHdr(ColorInfo) HDR}, and RGB BT.709 if not.
|
||||||
* @param sdrWorkingColorSpace The {@link WorkingColorSpace} to apply effects in.
|
* @param sdrWorkingColorSpace The {@link WorkingColorSpace} to apply effects in.
|
||||||
|
* @param sampleWithNearest Whether external textures require GL_NEAREST sampling.
|
||||||
* @throws VideoFrameProcessingException If a problem occurs while reading shader files or an
|
* @throws VideoFrameProcessingException If a problem occurs while reading shader files or an
|
||||||
* OpenGL operation fails or is unsupported.
|
* OpenGL operation fails or is unsupported.
|
||||||
*/
|
*/
|
||||||
@ -276,7 +277,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
Context context,
|
Context context,
|
||||||
ColorInfo inputColorInfo,
|
ColorInfo inputColorInfo,
|
||||||
ColorInfo outputColorInfo,
|
ColorInfo outputColorInfo,
|
||||||
@WorkingColorSpace int sdrWorkingColorSpace)
|
@WorkingColorSpace int sdrWorkingColorSpace,
|
||||||
|
boolean sampleWithNearest)
|
||||||
throws VideoFrameProcessingException {
|
throws VideoFrameProcessingException {
|
||||||
boolean isInputTransferHdr = ColorInfo.isTransferHdr(inputColorInfo);
|
boolean isInputTransferHdr = ColorInfo.isTransferHdr(inputColorInfo);
|
||||||
String vertexShaderFilePath =
|
String vertexShaderFilePath =
|
||||||
@ -304,6 +306,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
"uApplyHdrToSdrToneMapping",
|
"uApplyHdrToSdrToneMapping",
|
||||||
outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020 ? GL_TRUE : GL_FALSE);
|
outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020 ? GL_TRUE : GL_FALSE);
|
||||||
}
|
}
|
||||||
|
glProgram.setExternalTexturesRequireNearestSampling(sampleWithNearest);
|
||||||
|
|
||||||
return createWithSampler(glProgram, inputColorInfo, outputColorInfo, sdrWorkingColorSpace);
|
return createWithSampler(glProgram, inputColorInfo, outputColorInfo, sdrWorkingColorSpace);
|
||||||
}
|
}
|
||||||
|
@ -137,6 +137,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
private GlTextureProducer.@MonotonicNonNull Listener textureOutputListener;
|
private GlTextureProducer.@MonotonicNonNull Listener textureOutputListener;
|
||||||
private int textureOutputCapacity;
|
private int textureOutputCapacity;
|
||||||
private boolean requireRegisteringAllInputFrames;
|
private boolean requireRegisteringAllInputFrames;
|
||||||
|
private boolean experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
|
|
||||||
/** Creates an instance. */
|
/** Creates an instance. */
|
||||||
public Builder() {
|
public Builder() {
|
||||||
@ -151,6 +152,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
textureOutputListener = factory.textureOutputListener;
|
textureOutputListener = factory.textureOutputListener;
|
||||||
textureOutputCapacity = factory.textureOutputCapacity;
|
textureOutputCapacity = factory.textureOutputCapacity;
|
||||||
requireRegisteringAllInputFrames = !factory.repeatLastRegisteredFrame;
|
requireRegisteringAllInputFrames = !factory.repeatLastRegisteredFrame;
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix =
|
||||||
|
factory.experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -253,6 +256,21 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the {@link SurfaceTexture#getTransformMatrix(float[])} is adjusted to remove
|
||||||
|
* the scale that cuts off a 1- or 2-texel border around the edge of a crop.
|
||||||
|
*
|
||||||
|
* <p>When set, programs sampling GL_TEXTURE_EXTERNAL_OES from {@link SurfaceTexture} must not
|
||||||
|
* attempt to access data in any cropped region, including via GL_LINEAR resampling filter.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder setExperimentalAdjustSurfaceTextureTransformationMatrix(
|
||||||
|
boolean experimentalAdjustSurfaceTextureTransformationMatrix) {
|
||||||
|
this.experimentalAdjustSurfaceTextureTransformationMatrix =
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Builds an {@link DefaultVideoFrameProcessor.Factory} instance. */
|
/** Builds an {@link DefaultVideoFrameProcessor.Factory} instance. */
|
||||||
public DefaultVideoFrameProcessor.Factory build() {
|
public DefaultVideoFrameProcessor.Factory build() {
|
||||||
return new DefaultVideoFrameProcessor.Factory(
|
return new DefaultVideoFrameProcessor.Factory(
|
||||||
@ -261,7 +279,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
glObjectsProvider == null ? new DefaultGlObjectsProvider() : glObjectsProvider,
|
glObjectsProvider == null ? new DefaultGlObjectsProvider() : glObjectsProvider,
|
||||||
executorService,
|
executorService,
|
||||||
textureOutputListener,
|
textureOutputListener,
|
||||||
textureOutputCapacity);
|
textureOutputCapacity,
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +290,7 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
@Nullable private final ExecutorService executorService;
|
@Nullable private final ExecutorService executorService;
|
||||||
@Nullable private final GlTextureProducer.Listener textureOutputListener;
|
@Nullable private final GlTextureProducer.Listener textureOutputListener;
|
||||||
private final int textureOutputCapacity;
|
private final int textureOutputCapacity;
|
||||||
|
private final boolean experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
|
|
||||||
private Factory(
|
private Factory(
|
||||||
@WorkingColorSpace int sdrWorkingColorSpace,
|
@WorkingColorSpace int sdrWorkingColorSpace,
|
||||||
@ -278,13 +298,16 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
GlObjectsProvider glObjectsProvider,
|
GlObjectsProvider glObjectsProvider,
|
||||||
@Nullable ExecutorService executorService,
|
@Nullable ExecutorService executorService,
|
||||||
@Nullable GlTextureProducer.Listener textureOutputListener,
|
@Nullable GlTextureProducer.Listener textureOutputListener,
|
||||||
int textureOutputCapacity) {
|
int textureOutputCapacity,
|
||||||
|
boolean experimentalAdjustSurfaceTextureTransformationMatrix) {
|
||||||
this.sdrWorkingColorSpace = sdrWorkingColorSpace;
|
this.sdrWorkingColorSpace = sdrWorkingColorSpace;
|
||||||
this.repeatLastRegisteredFrame = repeatLastRegisteredFrame;
|
this.repeatLastRegisteredFrame = repeatLastRegisteredFrame;
|
||||||
this.glObjectsProvider = glObjectsProvider;
|
this.glObjectsProvider = glObjectsProvider;
|
||||||
this.executorService = executorService;
|
this.executorService = executorService;
|
||||||
this.textureOutputListener = textureOutputListener;
|
this.textureOutputListener = textureOutputListener;
|
||||||
this.textureOutputCapacity = textureOutputCapacity;
|
this.textureOutputCapacity = textureOutputCapacity;
|
||||||
|
this.experimentalAdjustSurfaceTextureTransformationMatrix =
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder buildUpon() {
|
public Builder buildUpon() {
|
||||||
@ -347,7 +370,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
glObjectsProvider,
|
glObjectsProvider,
|
||||||
textureOutputListener,
|
textureOutputListener,
|
||||||
textureOutputCapacity,
|
textureOutputCapacity,
|
||||||
repeatLastRegisteredFrame));
|
repeatLastRegisteredFrame,
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return defaultVideoFrameProcessorFuture.get();
|
return defaultVideoFrameProcessorFuture.get();
|
||||||
@ -715,7 +739,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
GlObjectsProvider glObjectsProvider,
|
GlObjectsProvider glObjectsProvider,
|
||||||
@Nullable GlTextureProducer.Listener textureOutputListener,
|
@Nullable GlTextureProducer.Listener textureOutputListener,
|
||||||
int textureOutputCapacity,
|
int textureOutputCapacity,
|
||||||
boolean repeatLastRegisteredFrame)
|
boolean repeatLastRegisteredFrame,
|
||||||
|
boolean experimentalAdjustSurfaceTextureTransformationMatrix)
|
||||||
throws GlUtil.GlException, VideoFrameProcessingException {
|
throws GlUtil.GlException, VideoFrameProcessingException {
|
||||||
EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
|
EGLDisplay eglDisplay = GlUtil.getDefaultEglDisplay();
|
||||||
int[] configAttributes =
|
int[] configAttributes =
|
||||||
@ -746,7 +771,8 @@ public final class DefaultVideoFrameProcessor implements VideoFrameProcessor {
|
|||||||
/* errorListenerExecutor= */ videoFrameProcessorListenerExecutor,
|
/* errorListenerExecutor= */ videoFrameProcessorListenerExecutor,
|
||||||
/* samplingShaderProgramErrorListener= */ listener::onError,
|
/* samplingShaderProgramErrorListener= */ listener::onError,
|
||||||
sdrWorkingColorSpace,
|
sdrWorkingColorSpace,
|
||||||
repeatLastRegisteredFrame);
|
repeatLastRegisteredFrame,
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix);
|
||||||
|
|
||||||
FinalShaderProgramWrapper finalShaderProgramWrapper =
|
FinalShaderProgramWrapper finalShaderProgramWrapper =
|
||||||
new FinalShaderProgramWrapper(
|
new FinalShaderProgramWrapper(
|
||||||
|
@ -24,9 +24,12 @@ import static androidx.media3.effect.DebugTraceUtil.COMPONENT_VFP;
|
|||||||
import static androidx.media3.effect.DebugTraceUtil.EVENT_QUEUE_FRAME;
|
import static androidx.media3.effect.DebugTraceUtil.EVENT_QUEUE_FRAME;
|
||||||
import static androidx.media3.effect.DebugTraceUtil.EVENT_SIGNAL_EOS;
|
import static androidx.media3.effect.DebugTraceUtil.EVENT_SIGNAL_EOS;
|
||||||
import static androidx.media3.effect.DebugTraceUtil.EVENT_SURFACE_TEXTURE_INPUT;
|
import static androidx.media3.effect.DebugTraceUtil.EVENT_SURFACE_TEXTURE_INPUT;
|
||||||
|
import static androidx.media3.effect.DebugTraceUtil.EVENT_SURFACE_TEXTURE_TRANSFORM_FIX;
|
||||||
|
import static java.lang.Math.abs;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.opengl.GLES31;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -54,6 +57,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
private static final String TAG = "ExtTexMgr";
|
private static final String TAG = "ExtTexMgr";
|
||||||
private static final String TIMER_THREAD_NAME = "ExtTexMgr:Timer";
|
private static final String TIMER_THREAD_NAME = "ExtTexMgr:Timer";
|
||||||
|
private static final int[] TRANSFORMATION_MATRIX_EXPECTED_ZERO_INDICES = {
|
||||||
|
2, 3, 6, 7, 8, 9, 11, 14
|
||||||
|
};
|
||||||
|
// In the worst case, we should be able to differentiate between numbers of the form
|
||||||
|
// A / B and (A + 1) / (B + 1) where A and B are around video resolution.
|
||||||
|
// For 8K, width = 7680.
|
||||||
|
// abs(7679 / 7680 - 7680 / 7681) > 1e-8. We pick EPSILON = 1e-9.
|
||||||
|
private static final float EPSILON = 1e-9f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The time out in milliseconds after calling signalEndOfCurrentInputStream after which the input
|
* The time out in milliseconds after calling signalEndOfCurrentInputStream after which the input
|
||||||
@ -79,6 +90,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
private final ScheduledExecutorService scheduledExecutorService;
|
private final ScheduledExecutorService scheduledExecutorService;
|
||||||
private final AtomicInteger externalShaderProgramInputCapacity;
|
private final AtomicInteger externalShaderProgramInputCapacity;
|
||||||
private final boolean repeatLastRegisteredFrame;
|
private final boolean repeatLastRegisteredFrame;
|
||||||
|
private final boolean experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
|
|
||||||
private int availableFrameCount;
|
private int availableFrameCount;
|
||||||
private boolean currentInputStreamEnded;
|
private boolean currentInputStreamEnded;
|
||||||
@ -104,6 +116,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* can call {@link #registerInputFrame(FrameInfo)} only once. Else, every input frame needs to
|
* can call {@link #registerInputFrame(FrameInfo)} only once. Else, every input frame needs to
|
||||||
* be {@linkplain #registerInputFrame(FrameInfo) registered} before they are made available on
|
* be {@linkplain #registerInputFrame(FrameInfo) registered} before they are made available on
|
||||||
* the {@linkplain #getInputSurface() input Surface}.
|
* the {@linkplain #getInputSurface() input Surface}.
|
||||||
|
* @param experimentalAdjustSurfaceTextureTransformationMatrix if {@code true}, the {@link
|
||||||
|
* SurfaceTexture#getTransformMatrix(float[])} will be adjusted to remove the scale that cuts
|
||||||
|
* off a 1- or 2-texel border around the edge of a crop.
|
||||||
* @throws VideoFrameProcessingException If a problem occurs while creating the external texture.
|
* @throws VideoFrameProcessingException If a problem occurs while creating the external texture.
|
||||||
*/
|
*/
|
||||||
// The onFrameAvailableListener will not be invoked until the constructor returns.
|
// The onFrameAvailableListener will not be invoked until the constructor returns.
|
||||||
@ -111,11 +126,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
public ExternalTextureManager(
|
public ExternalTextureManager(
|
||||||
GlObjectsProvider glObjectsProvider,
|
GlObjectsProvider glObjectsProvider,
|
||||||
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor,
|
VideoFrameProcessingTaskExecutor videoFrameProcessingTaskExecutor,
|
||||||
boolean repeatLastRegisteredFrame)
|
boolean repeatLastRegisteredFrame,
|
||||||
|
boolean experimentalAdjustSurfaceTextureTransformationMatrix)
|
||||||
throws VideoFrameProcessingException {
|
throws VideoFrameProcessingException {
|
||||||
super(videoFrameProcessingTaskExecutor);
|
super(videoFrameProcessingTaskExecutor);
|
||||||
this.glObjectsProvider = glObjectsProvider;
|
this.glObjectsProvider = glObjectsProvider;
|
||||||
this.repeatLastRegisteredFrame = repeatLastRegisteredFrame;
|
this.repeatLastRegisteredFrame = repeatLastRegisteredFrame;
|
||||||
|
this.experimentalAdjustSurfaceTextureTransformationMatrix =
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
try {
|
try {
|
||||||
externalTexId = GlUtil.createExternalTexture();
|
externalTexId = GlUtil.createExternalTexture();
|
||||||
} catch (GlUtil.GlException e) {
|
} catch (GlUtil.GlException e) {
|
||||||
@ -362,11 +380,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
|
|
||||||
externalShaderProgramInputCapacity.decrementAndGet();
|
externalShaderProgramInputCapacity.decrementAndGet();
|
||||||
surfaceTexture.getTransformMatrix(textureTransformMatrix);
|
surfaceTexture.getTransformMatrix(textureTransformMatrix);
|
||||||
checkNotNull(externalShaderProgram).setTextureTransformMatrix(textureTransformMatrix);
|
|
||||||
long frameTimeNs = surfaceTexture.getTimestamp();
|
long frameTimeNs = surfaceTexture.getTimestamp();
|
||||||
long offsetToAddUs = currentFrame.offsetToAddUs;
|
long offsetToAddUs = currentFrame.offsetToAddUs;
|
||||||
// Correct presentationTimeUs so that GlShaderPrograms don't see the stream offset.
|
// Correct presentationTimeUs so that GlShaderPrograms don't see the stream offset.
|
||||||
long presentationTimeUs = (frameTimeNs / 1000) + offsetToAddUs;
|
long presentationTimeUs = (frameTimeNs / 1000) + offsetToAddUs;
|
||||||
|
if (experimentalAdjustSurfaceTextureTransformationMatrix) {
|
||||||
|
removeSurfaceTextureScaleFromTransformMatrix(
|
||||||
|
textureTransformMatrix, presentationTimeUs, currentFrame.width, currentFrame.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkNotNull(externalShaderProgram).setTextureTransformMatrix(textureTransformMatrix);
|
||||||
checkNotNull(externalShaderProgram)
|
checkNotNull(externalShaderProgram)
|
||||||
.queueInputFrame(
|
.queueInputFrame(
|
||||||
glObjectsProvider,
|
glObjectsProvider,
|
||||||
@ -383,4 +406,154 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_QUEUE_FRAME, presentationTimeUs);
|
DebugTraceUtil.logEvent(COMPONENT_VFP, EVENT_QUEUE_FRAME, presentationTimeUs);
|
||||||
// If the queued frame is the last frame, end of stream will be signaled onInputFrameProcessed.
|
// If the queued frame is the last frame, end of stream will be signaled onInputFrameProcessed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts textureTransformMatrix inplace to remove any scaling applied by {@link
|
||||||
|
* SurfaceTexture#getTransformMatrix(float[])}. This method preserves cropping.
|
||||||
|
*
|
||||||
|
* <p>This method requires that textureTransformMatrix is a 4x4 column-major matrix that applies a
|
||||||
|
* linear scale and transform to OpenGL coordinates of the form (s, t, 0, 1).
|
||||||
|
*
|
||||||
|
* @param textureTransformMatrix The matrix to be modified inplace.
|
||||||
|
* @param presentationTimeUs The presentation time of the frame being processed.
|
||||||
|
* @param visibleWidth The expected visible width in pixels of the texture.
|
||||||
|
* @param visibleHeight The expected visible height in pixels of the texture.
|
||||||
|
*/
|
||||||
|
private static void removeSurfaceTextureScaleFromTransformMatrix(
|
||||||
|
float[] textureTransformMatrix,
|
||||||
|
long presentationTimeUs,
|
||||||
|
int visibleWidth,
|
||||||
|
int visibleHeight) {
|
||||||
|
boolean isMatrixUnexpected = false;
|
||||||
|
isMatrixUnexpected |= (textureTransformMatrix.length != 16);
|
||||||
|
for (int i : TRANSFORMATION_MATRIX_EXPECTED_ZERO_INDICES) {
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[i]) > EPSILON);
|
||||||
|
}
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[10] - 1f) > EPSILON);
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[15] - 1f) > EPSILON);
|
||||||
|
int widthScaleIndex = C.INDEX_UNSET;
|
||||||
|
int widthTranslationIndex = C.INDEX_UNSET;
|
||||||
|
int heightScaleIndex = C.INDEX_UNSET;
|
||||||
|
int heightTranslationIndex = C.INDEX_UNSET;
|
||||||
|
|
||||||
|
if (abs(textureTransformMatrix[0]) > EPSILON && abs(textureTransformMatrix[5]) > EPSILON) {
|
||||||
|
// 0 or 180 degree rotation. T maps width to width.
|
||||||
|
widthScaleIndex = 0;
|
||||||
|
widthTranslationIndex = 12;
|
||||||
|
heightScaleIndex = 5;
|
||||||
|
heightTranslationIndex = 13;
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[1]) > EPSILON);
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[4]) > EPSILON);
|
||||||
|
} else if (abs(textureTransformMatrix[1]) > EPSILON
|
||||||
|
&& abs(textureTransformMatrix[4]) > EPSILON) {
|
||||||
|
// 90 or 270 rotation. T swaps width and height.
|
||||||
|
widthScaleIndex = 1;
|
||||||
|
widthTranslationIndex = 13;
|
||||||
|
heightScaleIndex = 4;
|
||||||
|
heightTranslationIndex = 12;
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[0]) > EPSILON);
|
||||||
|
isMatrixUnexpected |= (abs(textureTransformMatrix[5]) > EPSILON);
|
||||||
|
} else {
|
||||||
|
isMatrixUnexpected = true;
|
||||||
|
}
|
||||||
|
if (isMatrixUnexpected) {
|
||||||
|
DebugTraceUtil.logEvent(
|
||||||
|
COMPONENT_EXTERNAL_TEXTURE_MANAGER,
|
||||||
|
EVENT_SURFACE_TEXTURE_TRANSFORM_FIX,
|
||||||
|
presentationTimeUs,
|
||||||
|
/* extraFormat= */ "Unable to apply SurfaceTexture fix");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float widthScale = textureTransformMatrix[widthScaleIndex];
|
||||||
|
float widthTranslation = textureTransformMatrix[widthTranslationIndex];
|
||||||
|
if (abs(widthScale) + EPSILON < 1f) {
|
||||||
|
// Applying a scale to the width means that some region of the texture must be cropped.
|
||||||
|
// Try to guess what the scale would be if SurfaceTexture didn't trim a few more pixels, in
|
||||||
|
// addition to the required crop.
|
||||||
|
float adjustedWidthScale =
|
||||||
|
Math.copySign(
|
||||||
|
guessScaleWithoutSurfaceTextureTrim(abs(widthScale), visibleWidth), widthScale);
|
||||||
|
float adjustedWidthTranslation = 0.5f * (widthScale - adjustedWidthScale) + widthTranslation;
|
||||||
|
DebugTraceUtil.logEvent(
|
||||||
|
COMPONENT_EXTERNAL_TEXTURE_MANAGER,
|
||||||
|
EVENT_SURFACE_TEXTURE_TRANSFORM_FIX,
|
||||||
|
presentationTimeUs,
|
||||||
|
/* extraFormat= */ "Width scale adjusted.");
|
||||||
|
textureTransformMatrix[widthScaleIndex] = adjustedWidthScale;
|
||||||
|
// Update translation to preserve midpoint. T(0.5, 0, 0, 1) remains fixed.
|
||||||
|
textureTransformMatrix[widthTranslationIndex] = adjustedWidthTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
float heightScale = textureTransformMatrix[heightScaleIndex];
|
||||||
|
float heightTranslation = textureTransformMatrix[heightTranslationIndex];
|
||||||
|
if (abs(heightScale) + EPSILON < 1f) {
|
||||||
|
// Applying a scale to the height means that some region of the texture must be cropped.
|
||||||
|
// Try to guess what the scale would be if SurfaceTexture didn't didn't trim a few more
|
||||||
|
// pixels, in addition to the required crop.
|
||||||
|
float adjustedHeightScale =
|
||||||
|
Math.copySign(
|
||||||
|
guessScaleWithoutSurfaceTextureTrim(abs(heightScale), visibleHeight), heightScale);
|
||||||
|
float adjustedHeightTranslation =
|
||||||
|
0.5f * (heightScale - adjustedHeightScale) + heightTranslation;
|
||||||
|
DebugTraceUtil.logEvent(
|
||||||
|
COMPONENT_EXTERNAL_TEXTURE_MANAGER,
|
||||||
|
EVENT_SURFACE_TEXTURE_TRANSFORM_FIX,
|
||||||
|
presentationTimeUs,
|
||||||
|
/* extraFormat= */ "Height scale adjusted.");
|
||||||
|
textureTransformMatrix[heightScaleIndex] = adjustedHeightScale;
|
||||||
|
// Update translation to preserve midpoint. T(0, 0.5, 0, 1) remains fixed.
|
||||||
|
textureTransformMatrix[heightTranslationIndex] = adjustedHeightTranslation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guess what the 1-D texture coordinate scale would be if SurfaceTexture was cropping without
|
||||||
|
* trimming a few extra pixels and stretching the image.
|
||||||
|
*
|
||||||
|
* <p>This method needs to guess:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>bufferSize = texture buffer size in texels. This should be the parameter value {@code
|
||||||
|
* visibleLength}, rounded up to a near multiple of 2.
|
||||||
|
* <p>Maybe it's rounded up to a multiple of 16 because of H.264 macroblock sizes. Maybe
|
||||||
|
* it's rounded up to 128 because of SIMD instructions.
|
||||||
|
* <p>bufferSize cannot be read reliably via {@link GLES31#glGetTexLevelParameteriv(int,
|
||||||
|
* int, int, int[], int)} across devices.
|
||||||
|
* <p>bufferSize cannot be read reliably from the decoder's {@link
|
||||||
|
* android.media.MediaFormat} across decoder implementations.
|
||||||
|
* <li>trim = number of pixels trimmed by {@link SurfaceTexture} in addition to the cropped
|
||||||
|
* region required for buffer SIMD alignment. As of the time of writing, this will be 0, 1
|
||||||
|
* or 2.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>This method will use the guessed bufferSize and trim values that most closely approximate
|
||||||
|
* surfaceTextureScale.
|
||||||
|
*
|
||||||
|
* @param surfaceTextureScale the absolute value of the scaling factor from {@link
|
||||||
|
* SurfaceTexture#getTransformMatrix(float[])}. It has the form {@code (visibleLength - trim)
|
||||||
|
* / bufferSize}.
|
||||||
|
* @param visibleLength Expected size in pixels of the visible range.
|
||||||
|
* @return Scale without trim, of the form visibleLength / bufferSize.
|
||||||
|
*/
|
||||||
|
private static float guessScaleWithoutSurfaceTextureTrim(
|
||||||
|
float surfaceTextureScale, int visibleLength) {
|
||||||
|
float bestGuess = 1;
|
||||||
|
float scaleWithoutTrim = 1;
|
||||||
|
|
||||||
|
for (int align = 2; align <= 256; align *= 2) {
|
||||||
|
int candidateBufferSize = ((visibleLength + align - 1) / align) * align;
|
||||||
|
for (int trimmedPixels = 0; trimmedPixels <= 2; trimmedPixels++) {
|
||||||
|
float guess = ((float) visibleLength - trimmedPixels) / candidateBufferSize;
|
||||||
|
if (abs(guess - surfaceTextureScale) < abs(bestGuess - surfaceTextureScale)) {
|
||||||
|
bestGuess = guess;
|
||||||
|
scaleWithoutTrim = (float) visibleLength / candidateBufferSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (abs(bestGuess - surfaceTextureScale) > EPSILON) {
|
||||||
|
// Best guess is too far off. Accept that we'll scale.
|
||||||
|
return surfaceTextureScale;
|
||||||
|
}
|
||||||
|
return scaleWithoutTrim;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
private final Executor errorListenerExecutor;
|
private final Executor errorListenerExecutor;
|
||||||
private final SparseArray<Input> inputs;
|
private final SparseArray<Input> inputs;
|
||||||
private final @WorkingColorSpace int sdrWorkingColorSpace;
|
private final @WorkingColorSpace int sdrWorkingColorSpace;
|
||||||
|
private final boolean experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
|
|
||||||
private @MonotonicNonNull GlShaderProgram downstreamShaderProgram;
|
private @MonotonicNonNull GlShaderProgram downstreamShaderProgram;
|
||||||
private @MonotonicNonNull TextureManager activeTextureManager;
|
private @MonotonicNonNull TextureManager activeTextureManager;
|
||||||
@ -65,7 +66,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
Executor errorListenerExecutor,
|
Executor errorListenerExecutor,
|
||||||
GlShaderProgram.ErrorListener samplingShaderProgramErrorListener,
|
GlShaderProgram.ErrorListener samplingShaderProgramErrorListener,
|
||||||
@WorkingColorSpace int sdrWorkingColorSpace,
|
@WorkingColorSpace int sdrWorkingColorSpace,
|
||||||
boolean repeatLastRegisteredFrame)
|
boolean repeatLastRegisteredFrame,
|
||||||
|
boolean experimentalAdjustSurfaceTextureTransformationMatrix)
|
||||||
throws VideoFrameProcessingException {
|
throws VideoFrameProcessingException {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.outputColorInfo = outputColorInfo;
|
this.outputColorInfo = outputColorInfo;
|
||||||
@ -75,13 +77,18 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
this.samplingShaderProgramErrorListener = samplingShaderProgramErrorListener;
|
this.samplingShaderProgramErrorListener = samplingShaderProgramErrorListener;
|
||||||
this.inputs = new SparseArray<>();
|
this.inputs = new SparseArray<>();
|
||||||
this.sdrWorkingColorSpace = sdrWorkingColorSpace;
|
this.sdrWorkingColorSpace = sdrWorkingColorSpace;
|
||||||
|
this.experimentalAdjustSurfaceTextureTransformationMatrix =
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix;
|
||||||
|
|
||||||
// TODO(b/274109008): Investigate lazy instantiating the texture managers.
|
// TODO(b/274109008): Investigate lazy instantiating the texture managers.
|
||||||
inputs.put(
|
inputs.put(
|
||||||
INPUT_TYPE_SURFACE,
|
INPUT_TYPE_SURFACE,
|
||||||
new Input(
|
new Input(
|
||||||
new ExternalTextureManager(
|
new ExternalTextureManager(
|
||||||
glObjectsProvider, videoFrameProcessingTaskExecutor, repeatLastRegisteredFrame)));
|
glObjectsProvider,
|
||||||
|
videoFrameProcessingTaskExecutor,
|
||||||
|
repeatLastRegisteredFrame,
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix)));
|
||||||
inputs.put(
|
inputs.put(
|
||||||
INPUT_TYPE_BITMAP,
|
INPUT_TYPE_BITMAP,
|
||||||
new Input(new BitmapTextureManager(glObjectsProvider, videoFrameProcessingTaskExecutor)));
|
new Input(new BitmapTextureManager(glObjectsProvider, videoFrameProcessingTaskExecutor)));
|
||||||
@ -99,7 +106,11 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
|||||||
case INPUT_TYPE_SURFACE:
|
case INPUT_TYPE_SURFACE:
|
||||||
samplingShaderProgram =
|
samplingShaderProgram =
|
||||||
DefaultShaderProgram.createWithExternalSampler(
|
DefaultShaderProgram.createWithExternalSampler(
|
||||||
context, inputColorInfo, outputColorInfo, sdrWorkingColorSpace);
|
context,
|
||||||
|
inputColorInfo,
|
||||||
|
outputColorInfo,
|
||||||
|
sdrWorkingColorSpace,
|
||||||
|
experimentalAdjustSurfaceTextureTransformationMatrix);
|
||||||
break;
|
break;
|
||||||
case INPUT_TYPE_BITMAP:
|
case INPUT_TYPE_BITMAP:
|
||||||
case INPUT_TYPE_TEXTURE_ID:
|
case INPUT_TYPE_TEXTURE_ID:
|
||||||
|
@ -316,7 +316,7 @@ public final class VideoDecoderGLSurfaceView extends GLSurfaceView
|
|||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
GLES20.glUniform1i(program.getUniformLocation(TEXTURE_UNIFORMS[i]), i);
|
GLES20.glUniform1i(program.getUniformLocation(TEXTURE_UNIFORMS[i]), i);
|
||||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
|
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
|
||||||
GlUtil.bindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]);
|
GlUtil.bindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i], GLES20.GL_LINEAR);
|
||||||
}
|
}
|
||||||
GlUtil.checkGlError();
|
GlUtil.checkGlError();
|
||||||
} catch (GlUtil.GlException e) {
|
} catch (GlUtil.GlException e) {
|
||||||
|
After Width: | Height: | Size: 294 KiB |
After Width: | Height: | Size: 304 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 133 KiB |
After Width: | Height: | Size: 338 KiB |
After Width: | Height: | Size: 220 KiB |
After Width: | Height: | Size: 91 KiB |
@ -137,6 +137,16 @@ public final class AndroidTestUtil {
|
|||||||
.setFrameRate(30.0f)
|
.setFrameRate(30.0f)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
public static final String MP4_ASSET_CHECKERBOARD_VIDEO_URI_STRING =
|
||||||
|
"asset:///media/mp4/checkerboard_854x356_avc_baseline.mp4";
|
||||||
|
public static final Format MP4_ASSET_CHECKERBOARD_VIDEO_FORMAT =
|
||||||
|
new Format.Builder()
|
||||||
|
.setSampleMimeType(VIDEO_H264)
|
||||||
|
.setWidth(854)
|
||||||
|
.setHeight(356)
|
||||||
|
.setFrameRate(25.0f)
|
||||||
|
.build();
|
||||||
|
|
||||||
public static final String MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING =
|
public static final String MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING =
|
||||||
"asset:///media/mp4/sample_with_increasing_timestamps.mp4";
|
"asset:///media/mp4/sample_with_increasing_timestamps.mp4";
|
||||||
public static final Format MP4_ASSET_WITH_INCREASING_TIMESTAMPS_FORMAT =
|
public static final Format MP4_ASSET_WITH_INCREASING_TIMESTAMPS_FORMAT =
|
||||||
@ -246,8 +256,8 @@ public final class AndroidTestUtil {
|
|||||||
public static final Format MP4_ASSET_AV1_2_SECOND_HDR10_FORMAT =
|
public static final Format MP4_ASSET_AV1_2_SECOND_HDR10_FORMAT =
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
.setSampleMimeType(VIDEO_AV1)
|
.setSampleMimeType(VIDEO_AV1)
|
||||||
.setWidth(1920)
|
.setWidth(720)
|
||||||
.setHeight(1080)
|
.setHeight(1280)
|
||||||
.setFrameRate(59.94f)
|
.setFrameRate(59.94f)
|
||||||
.setColorInfo(
|
.setColorInfo(
|
||||||
new ColorInfo.Builder()
|
new ColorInfo.Builder()
|
||||||
|
@ -20,14 +20,20 @@ import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIX
|
|||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
||||||
|
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo;
|
||||||
import static com.google.common.truth.Truth.assertWithMessage;
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.Effect;
|
import androidx.media3.common.Effect;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.effect.DefaultVideoFrameProcessor;
|
||||||
import androidx.media3.effect.Presentation;
|
import androidx.media3.effect.Presentation;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -35,9 +41,19 @@ import java.util.List;
|
|||||||
/** Utility class for checking testing {@link EditedMediaItemSequence} instances. */
|
/** Utility class for checking testing {@link EditedMediaItemSequence} instances. */
|
||||||
public final class SequenceEffectTestUtil {
|
public final class SequenceEffectTestUtil {
|
||||||
public static final ImmutableList<Effect> NO_EFFECT = ImmutableList.of();
|
public static final ImmutableList<Effect> NO_EFFECT = ImmutableList.of();
|
||||||
|
public static final long SINGLE_30_FPS_VIDEO_FRAME_THRESHOLD_MS = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Luma PSNR values between 30 and 50 are considered good for lossy compression (See <a
|
||||||
|
* href="https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio#Quality_estimation_with_PSNR">Quality
|
||||||
|
* estimation with PSNR</a> ). Other than that, the values in this files are pretty arbitrary -- 1
|
||||||
|
* more and tests start failing on some devices.
|
||||||
|
*/
|
||||||
|
public static final float PSNR_THRESHOLD = 35f;
|
||||||
|
|
||||||
|
public static final float PSNR_THRESHOLD_HD = 41f;
|
||||||
private static final String PNG_ASSET_BASE_PATH =
|
private static final String PNG_ASSET_BASE_PATH =
|
||||||
"test-generated-goldens/transformer_sequence_effect_test";
|
"test-generated-goldens/transformer_sequence_effect_test";
|
||||||
public static final long SINGLE_30_FPS_VIDEO_FRAME_THRESHOLD_MS = 50;
|
|
||||||
|
|
||||||
private SequenceEffectTestUtil() {}
|
private SequenceEffectTestUtil() {}
|
||||||
|
|
||||||
@ -120,4 +136,106 @@ public final class SequenceEffectTestUtil {
|
|||||||
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA);
|
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the first frame extracted from the video in filePath matches output in {@link
|
||||||
|
* #PNG_ASSET_BASE_PATH}/{@code testId}_0.png.
|
||||||
|
*
|
||||||
|
* <p>Also saves the first frame as a bitmap, in case they differ from expected.
|
||||||
|
*/
|
||||||
|
public static void assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
Context context, String testId, String filePath, float psnrThreshold)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
Bitmap firstEncodedFrame = extractBitmapsFromVideo(context, filePath).get(0);
|
||||||
|
assertBitmapsMatchExpectedPsnrAndSave(
|
||||||
|
ImmutableList.of(firstEncodedFrame), testId, psnrThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertBitmapsMatchExpectedPsnrAndSave(
|
||||||
|
List<Bitmap> actualBitmaps, String testId, float psnrThreshold) throws IOException {
|
||||||
|
for (int i = 0; i < actualBitmaps.size(); i++) {
|
||||||
|
maybeSaveTestBitmap(
|
||||||
|
testId, /* bitmapLabel= */ String.valueOf(i), actualBitmaps.get(i), /* path= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < actualBitmaps.size(); i++) {
|
||||||
|
String subTestId = testId + "_" + i;
|
||||||
|
String expectedPath = Util.formatInvariant("%s/%s.png", PNG_ASSET_BASE_PATH, subTestId);
|
||||||
|
Bitmap expectedBitmap = readBitmap(expectedPath);
|
||||||
|
|
||||||
|
assertBitmapsAreSimilar(expectedBitmap, actualBitmaps.get(i), psnrThreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the MediaCodecInfo decoder is known to produce incorrect colours on this
|
||||||
|
* device.
|
||||||
|
*
|
||||||
|
* <p>Washed out colours are probably caused by incorrect color space assumptions by MediaCodec.
|
||||||
|
*/
|
||||||
|
public static boolean decoderProducesWashedOutColours(MediaCodecInfo mediaCodecInfo) {
|
||||||
|
return mediaCodecInfo.name.equals("OMX.google.h264.decoder")
|
||||||
|
&& (Util.MODEL.equals("ANE-LX1")
|
||||||
|
|| Util.MODEL.equals("MHA-L29")
|
||||||
|
|| Util.MODEL.equals("COR-L29"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to export the {@link Composition} with a high quality {@link Transformer} created via
|
||||||
|
* {@link #createHqTransformer} with the requested {@code decoderMediaCodecInfo}.
|
||||||
|
*
|
||||||
|
* @return The {@link ExportTestResult} when successful, or {@code null} if decoding fails.
|
||||||
|
* @throws Exception The cause of the export not completing.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static ExportTestResult tryToExportCompositionWithDecoder(
|
||||||
|
String testId, Context context, MediaCodecInfo decoderMediaCodecInfo, Composition composition)
|
||||||
|
throws Exception {
|
||||||
|
try {
|
||||||
|
return new TransformerAndroidTestRunner.Builder(
|
||||||
|
context, createHqTransformer(context, decoderMediaCodecInfo))
|
||||||
|
.build()
|
||||||
|
.run(testId, composition);
|
||||||
|
} catch (ExportException exportException) {
|
||||||
|
if (exportException.errorCode == ExportException.ERROR_CODE_DECODING_FAILED
|
||||||
|
|| exportException.errorCode == ExportException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED
|
||||||
|
|| exportException.errorCode == ExportException.ERROR_CODE_DECODER_INIT_FAILED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw exportException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a high quality {@link Transformer} instance.
|
||||||
|
*
|
||||||
|
* <p>The {@link Transformer} is configured to select a specific decoder, use experimental
|
||||||
|
* high-quality {@link DefaultVideoFrameProcessor} configuration, and a large value for {@link
|
||||||
|
* VideoEncoderSettings#bitrate}.
|
||||||
|
*/
|
||||||
|
public static Transformer createHqTransformer(
|
||||||
|
Context context, MediaCodecInfo decoderMediaCodecInfo) {
|
||||||
|
Codec.DecoderFactory decoderFactory =
|
||||||
|
new DefaultDecoderFactory.Builder(context)
|
||||||
|
.setMediaCodecSelector(
|
||||||
|
(mimeType, requiresSecureDecoder, requiresTunnelingDecoder) ->
|
||||||
|
ImmutableList.of(decoderMediaCodecInfo))
|
||||||
|
.build();
|
||||||
|
AssetLoader.Factory assetLoaderFactory =
|
||||||
|
new DefaultAssetLoaderFactory(context, decoderFactory, Clock.DEFAULT);
|
||||||
|
DefaultVideoFrameProcessor.Factory videoFrameProcessorFactory =
|
||||||
|
new DefaultVideoFrameProcessor.Factory.Builder()
|
||||||
|
.setExperimentalAdjustSurfaceTextureTransformationMatrix(true)
|
||||||
|
.build();
|
||||||
|
Codec.EncoderFactory encoderFactory =
|
||||||
|
new DefaultEncoderFactory.Builder(context)
|
||||||
|
.setRequestedVideoEncoderSettings(
|
||||||
|
new VideoEncoderSettings.Builder().setBitrate(30_000_000).build())
|
||||||
|
.build();
|
||||||
|
return new Transformer.Builder(context)
|
||||||
|
.setAssetLoaderFactory(assetLoaderFactory)
|
||||||
|
.setVideoFrameProcessorFactory(videoFrameProcessorFactory)
|
||||||
|
.setEncoderFactory(new AndroidTestUtil.ForceEncodeEncoderFactory(encoderFactory))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,40 +19,59 @@ package androidx.media3.transformer;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Util.SDK_INT;
|
import static androidx.media3.common.util.Util.SDK_INT;
|
||||||
|
import static androidx.media3.effect.DebugTraceUtil.EVENT_SURFACE_TEXTURE_TRANSFORM_FIX;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.BT601_ASSET_FORMAT;
|
import static androidx.media3.transformer.AndroidTestUtil.BT601_ASSET_FORMAT;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.BT601_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.BT601_ASSET_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.JPG_PORTRAIT_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.JPG_PORTRAIT_ASSET_URI_STRING;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_AV1_VIDEO_FORMAT;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_AV1_VIDEO_URI_STRING;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_CHECKERBOARD_VIDEO_FORMAT;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_CHECKERBOARD_VIDEO_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_FORMAT;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_FORMAT;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_FORMAT;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_PORTRAIT_ASSET_FORMAT;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_PORTRAIT_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_PORTRAIT_ASSET_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo;
|
import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.NO_EFFECT;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.NO_EFFECT;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.PSNR_THRESHOLD;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.PSNR_THRESHOLD_HD;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.SINGLE_30_FPS_VIDEO_FRAME_THRESHOLD_MS;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.SINGLE_30_FPS_VIDEO_FRAME_THRESHOLD_MS;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.assertBitmapsMatchExpectedAndSave;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.assertBitmapsMatchExpectedAndSave;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.assertFirstFrameMatchesExpectedPsnrAndSave;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.clippedVideo;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.clippedVideo;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.createComposition;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.createComposition;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.decoderProducesWashedOutColours;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.oneFrameFromImage;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.oneFrameFromImage;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.tryToExportCompositionWithDecoder;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assume.assumeFalse;
|
import static org.junit.Assume.assumeFalse;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Effect;
|
import androidx.media3.common.Effect;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.effect.BitmapOverlay;
|
import androidx.media3.effect.BitmapOverlay;
|
||||||
|
import androidx.media3.effect.DebugTraceUtil;
|
||||||
import androidx.media3.effect.DefaultVideoFrameProcessor;
|
import androidx.media3.effect.DefaultVideoFrameProcessor;
|
||||||
import androidx.media3.effect.OverlayEffect;
|
import androidx.media3.effect.OverlayEffect;
|
||||||
import androidx.media3.effect.Presentation;
|
import androidx.media3.effect.Presentation;
|
||||||
import androidx.media3.effect.RgbFilter;
|
import androidx.media3.effect.RgbFilter;
|
||||||
import androidx.media3.effect.ScaleAndRotateTransformation;
|
import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -80,6 +99,11 @@ public final class TransformerSequenceEffectTest {
|
|||||||
testId = testName.getMethodName();
|
testId = testName.getMethodName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
DebugTraceUtil.enableTracing = false;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void export_withNoCompositionPresentationAndWithPerMediaItemEffects() throws Exception {
|
public void export_withNoCompositionPresentationAndWithPerMediaItemEffects() throws Exception {
|
||||||
assumeFormatsSupported(
|
assumeFormatsSupported(
|
||||||
@ -119,6 +143,205 @@ public final class TransformerSequenceEffectTest {
|
|||||||
extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId);
|
extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export1080x720_withAllAvailableDecoders_doesNotStretchOutputOnAny() throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context, testId, /* inputFormat= */ MP4_ASSET_FORMAT, /* outputFormat= */ MP4_ASSET_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(MP4_ASSET_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
/* presentation= */ null,
|
||||||
|
clippedVideo(
|
||||||
|
MP4_ASSET_URI_STRING, NO_EFFECT, /* endPositionMs= */ C.MILLIS_PER_SECOND / 4));
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
if (decoderProducesWashedOutColours(mediaCodecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD_HD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export720x1080_withAllAvailableDecoders_doesNotStretchOutputOnAny() throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_PORTRAIT_ASSET_FORMAT,
|
||||||
|
/* outputFormat= */ MP4_PORTRAIT_ASSET_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(MP4_PORTRAIT_ASSET_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
/* presentation= */ null,
|
||||||
|
clippedVideo(
|
||||||
|
MP4_PORTRAIT_ASSET_URI_STRING,
|
||||||
|
NO_EFFECT,
|
||||||
|
/* endPositionMs= */ C.MILLIS_PER_SECOND / 4));
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
if (decoderProducesWashedOutColours(mediaCodecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD_HD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export640x428_withAllAvailableDecoders_doesNotStretchOutputOnAny() throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ BT601_ASSET_FORMAT,
|
||||||
|
/* outputFormat= */ BT601_ASSET_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(BT601_ASSET_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
/* presentation= */ null,
|
||||||
|
clippedVideo(
|
||||||
|
BT601_ASSET_URI_STRING, NO_EFFECT, /* endPositionMs= */ C.MILLIS_PER_SECOND / 4));
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
if (decoderProducesWashedOutColours(mediaCodecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export1080x720Av1_withAllAvailableDecoders_doesNotStretchOutputOnAny()
|
||||||
|
throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET_AV1_VIDEO_FORMAT,
|
||||||
|
/* outputFormat= */ MP4_ASSET_AV1_VIDEO_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(MP4_ASSET_AV1_VIDEO_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
/* presentation= */ null,
|
||||||
|
clippedVideo(
|
||||||
|
MP4_ASSET_AV1_VIDEO_URI_STRING,
|
||||||
|
NO_EFFECT,
|
||||||
|
/* endPositionMs= */ C.MILLIS_PER_SECOND / 4));
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
if (decoderProducesWashedOutColours(mediaCodecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD_HD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export854x356_withAllAvailableDecoders_doesNotStretchOutputOnAny() throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET_CHECKERBOARD_VIDEO_FORMAT,
|
||||||
|
/* outputFormat= */ MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(MP4_ASSET_CHECKERBOARD_VIDEO_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
Presentation.createForWidthAndHeight(
|
||||||
|
/* width= */ 320, /* height= */ 240, Presentation.LAYOUT_SCALE_TO_FIT),
|
||||||
|
clippedVideo(
|
||||||
|
MP4_ASSET_CHECKERBOARD_VIDEO_URI_STRING,
|
||||||
|
NO_EFFECT,
|
||||||
|
/* endPositionMs= */ C.MILLIS_PER_SECOND / 4));
|
||||||
|
DebugTraceUtil.enableTracing = true;
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
if (decoderProducesWashedOutColours(mediaCodecInfo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
|
||||||
|
String traceSummary = DebugTraceUtil.generateTraceSummary();
|
||||||
|
assertThat(traceSummary.indexOf(EVENT_SURFACE_TEXTURE_TRANSFORM_FIX)).isNotEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void export_withCompositionPresentationAndWithPerMediaItemEffects() throws Exception {
|
public void export_withCompositionPresentationAndWithPerMediaItemEffects() throws Exception {
|
||||||
// Reference: b/296225823#comment5
|
// Reference: b/296225823#comment5
|
||||||
|
@ -18,15 +18,23 @@
|
|||||||
package androidx.media3.transformer.mh;
|
package androidx.media3.transformer.mh;
|
||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_1080P_5_SECOND_HLG10;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_720P_4_SECOND_HDR10;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_720P_4_SECOND_HDR10;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_720P_4_SECOND_HDR10_FORMAT;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_720P_4_SECOND_HDR10_FORMAT;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_AV1_2_SECOND_HDR10;
|
||||||
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_AV1_2_SECOND_HDR10_FORMAT;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_PORTRAIT_ASSET_URI_STRING;
|
import static androidx.media3.transformer.AndroidTestUtil.MP4_PORTRAIT_ASSET_URI_STRING;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
|
||||||
import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo;
|
import static androidx.media3.transformer.AndroidTestUtil.extractBitmapsFromVideo;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.NO_EFFECT;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.PSNR_THRESHOLD_HD;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.SINGLE_30_FPS_VIDEO_FRAME_THRESHOLD_MS;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.SINGLE_30_FPS_VIDEO_FRAME_THRESHOLD_MS;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.assertBitmapsMatchExpectedAndSave;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.assertBitmapsMatchExpectedAndSave;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.assertFirstFrameMatchesExpectedPsnrAndSave;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.clippedVideo;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.clippedVideo;
|
||||||
import static androidx.media3.transformer.SequenceEffectTestUtil.createComposition;
|
import static androidx.media3.transformer.SequenceEffectTestUtil.createComposition;
|
||||||
|
import static androidx.media3.transformer.SequenceEffectTestUtil.tryToExportCompositionWithDecoder;
|
||||||
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceDoesNotSupportHdrEditing;
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceDoesNotSupportHdrEditing;
|
||||||
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsHdrEditing;
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsHdrEditing;
|
||||||
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
|
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
|
||||||
@ -34,11 +42,14 @@ import static com.google.common.truth.Truth.assertThat;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Effect;
|
import androidx.media3.common.Effect;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.effect.Presentation;
|
import androidx.media3.effect.Presentation;
|
||||||
import androidx.media3.effect.RgbFilter;
|
import androidx.media3.effect.RgbFilter;
|
||||||
import androidx.media3.effect.ScaleAndRotateTransformation;
|
import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||||
import androidx.media3.transformer.Composition;
|
import androidx.media3.transformer.Composition;
|
||||||
import androidx.media3.transformer.EditedMediaItemSequence;
|
import androidx.media3.transformer.EditedMediaItemSequence;
|
||||||
import androidx.media3.transformer.ExportException;
|
import androidx.media3.transformer.ExportException;
|
||||||
@ -48,6 +59,7 @@ import androidx.media3.transformer.TransformerAndroidTestRunner;
|
|||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.util.List;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -179,4 +191,78 @@ public final class TransformerSequenceEffectTestWithHdr {
|
|||||||
assertBitmapsMatchExpectedAndSave(
|
assertBitmapsMatchExpectedAndSave(
|
||||||
extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId);
|
extractBitmapsFromVideo(context, checkNotNull(result.filePath)), testId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export1920x1080Hlg_withAllAvailableDecoders_doesNotStretchOutputOnAny()
|
||||||
|
throws Exception {
|
||||||
|
assumeDeviceSupportsHdrEditing(testId, MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT);
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT,
|
||||||
|
/* outputFormat= */ MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
/* presentation= */ null,
|
||||||
|
clippedVideo(
|
||||||
|
MP4_ASSET_1080P_5_SECOND_HLG10,
|
||||||
|
NO_EFFECT,
|
||||||
|
/* endPositionMs= */ C.MILLIS_PER_SECOND / 4));
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD_HD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void export720x1280Av1Hdr10_withAllAvailableDecoders_doesNotStretchOutputOnAny()
|
||||||
|
throws Exception {
|
||||||
|
assumeFormatsSupported(
|
||||||
|
context,
|
||||||
|
testId,
|
||||||
|
/* inputFormat= */ MP4_ASSET_AV1_2_SECOND_HDR10_FORMAT,
|
||||||
|
/* outputFormat= */ MP4_ASSET_AV1_2_SECOND_HDR10_FORMAT);
|
||||||
|
List<MediaCodecInfo> mediaCodecInfoList =
|
||||||
|
MediaCodecSelector.DEFAULT.getDecoderInfos(
|
||||||
|
checkNotNull(MP4_ASSET_AV1_2_SECOND_HDR10_FORMAT.sampleMimeType),
|
||||||
|
/* requiresSecureDecoder= */ false,
|
||||||
|
/* requiresTunnelingDecoder= */ false);
|
||||||
|
Composition composition =
|
||||||
|
createComposition(
|
||||||
|
/* presentation= */ null,
|
||||||
|
clippedVideo(MP4_ASSET_AV1_2_SECOND_HDR10, NO_EFFECT, C.MILLIS_PER_SECOND / 4));
|
||||||
|
|
||||||
|
boolean atLeastOneDecoderSucceeds = false;
|
||||||
|
for (MediaCodecInfo mediaCodecInfo : mediaCodecInfoList) {
|
||||||
|
@Nullable
|
||||||
|
ExportTestResult result =
|
||||||
|
tryToExportCompositionWithDecoder(testId, context, mediaCodecInfo, composition);
|
||||||
|
if (result == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
atLeastOneDecoderSucceeds = true;
|
||||||
|
|
||||||
|
assertThat(checkNotNull(result).filePath).isNotNull();
|
||||||
|
assertFirstFrameMatchesExpectedPsnrAndSave(
|
||||||
|
context, testId, checkNotNull(result.filePath), PSNR_THRESHOLD_HD);
|
||||||
|
}
|
||||||
|
assertThat(atLeastOneDecoderSucceeds).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,12 +75,14 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory {
|
|||||||
private Listener listener;
|
private Listener listener;
|
||||||
private boolean enableDecoderFallback;
|
private boolean enableDecoderFallback;
|
||||||
private @C.Priority int codecPriority;
|
private @C.Priority int codecPriority;
|
||||||
|
private MediaCodecSelector mediaCodecSelector;
|
||||||
|
|
||||||
/** Creates a new {@link Builder}. */
|
/** Creates a new {@link Builder}. */
|
||||||
public Builder(Context context) {
|
public Builder(Context context) {
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
listener = (codecName, codecInitializationExceptions) -> {};
|
listener = (codecName, codecInitializationExceptions) -> {};
|
||||||
codecPriority = C.PRIORITY_PROCESSING_FOREGROUND;
|
codecPriority = C.PRIORITY_PROCESSING_FOREGROUND;
|
||||||
|
mediaCodecSelector = MediaCodecSelector.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the {@link Listener}. */
|
/** Sets the {@link Listener}. */
|
||||||
@ -128,6 +130,17 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link MediaCodecSelector} used when selecting a decoder.
|
||||||
|
*
|
||||||
|
* <p>The default value is {@link MediaCodecSelector#DEFAULT}
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) {
|
||||||
|
this.mediaCodecSelector = mediaCodecSelector;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates an instance of {@link DefaultDecoderFactory}, using defaults if values are unset. */
|
/** Creates an instance of {@link DefaultDecoderFactory}, using defaults if values are unset. */
|
||||||
public DefaultDecoderFactory build() {
|
public DefaultDecoderFactory build() {
|
||||||
return new DefaultDecoderFactory(this);
|
return new DefaultDecoderFactory(this);
|
||||||
@ -138,6 +151,7 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory {
|
|||||||
private final boolean enableDecoderFallback;
|
private final boolean enableDecoderFallback;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
private final @C.Priority int codecPriority;
|
private final @C.Priority int codecPriority;
|
||||||
|
private final MediaCodecSelector mediaCodecSelector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use {@link Builder} instead.
|
* @deprecated Use {@link Builder} instead.
|
||||||
@ -169,6 +183,7 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory {
|
|||||||
this.enableDecoderFallback = builder.enableDecoderFallback;
|
this.enableDecoderFallback = builder.enableDecoderFallback;
|
||||||
this.listener = builder.listener;
|
this.listener = builder.listener;
|
||||||
this.codecPriority = builder.codecPriority;
|
this.codecPriority = builder.codecPriority;
|
||||||
|
this.mediaCodecSelector = builder.mediaCodecSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -241,7 +256,7 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory {
|
|||||||
decoderInfos =
|
decoderInfos =
|
||||||
MediaCodecUtil.getDecoderInfosSortedByFormatSupport(
|
MediaCodecUtil.getDecoderInfosSortedByFormatSupport(
|
||||||
MediaCodecUtil.getDecoderInfosSoftMatch(
|
MediaCodecUtil.getDecoderInfosSoftMatch(
|
||||||
MediaCodecSelector.DEFAULT,
|
mediaCodecSelector,
|
||||||
format,
|
format,
|
||||||
/* requiresSecureDecoder= */ false,
|
/* requiresSecureDecoder= */ false,
|
||||||
/* requiresTunnelingDecoder= */ false),
|
/* requiresTunnelingDecoder= */ false),
|
||||||
|