diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/Crop.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/Crop.java index b302b07989..4cd1d3af55 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/Crop.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/Crop.java @@ -91,4 +91,12 @@ public final class Crop implements MatrixTransformation { public Matrix getMatrix(long presentationTimeUs) { return checkStateNotNull(transformationMatrix, "configure must be called first"); } + + @Override + public boolean isNoOp(int inputWidth, int inputHeight) { + Size outputSize = configure(inputWidth, inputHeight); + return checkStateNotNull(transformationMatrix).isIdentity() + && inputWidth == outputSize.getWidth() + && inputHeight == outputSize.getHeight(); + } } diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffect.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffect.java index 2e273e7ebc..282f3d9686 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffect.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffect.java @@ -38,4 +38,19 @@ public interface GlEffect extends Effect { */ GlShaderProgram toGlShaderProgram(Context context, boolean useHdr) throws FrameProcessingException; + + /** + * Returns whether a {@link GlEffect} applies no change at every timestamp. + * + *

This can be used as a hint to skip this instance. + * + * @param inputWidth The input frame width, in pixels. + * @param inputHeight The input frame height, in pixels. + */ + default boolean isNoOp(int inputWidth, int inputHeight) { + // TODO(b/265927935): Generalize this logic by implementing this method on all + // subclasses, and deleting the default implementation here. Otherwise, some no-op effects may + // not be properly detected or handled. + return false; + } } diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/Presentation.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/Presentation.java index 9c82e156e6..9c1b352b0c 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/Presentation.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/Presentation.java @@ -212,6 +212,14 @@ public final class Presentation implements MatrixTransformation { return checkStateNotNull(transformationMatrix, "configure must be called first"); } + @Override + public boolean isNoOp(int inputWidth, int inputHeight) { + configure(inputWidth, inputHeight); + return checkStateNotNull(transformationMatrix).isIdentity() + && inputWidth == Math.round(outputWidth) + && inputHeight == Math.round(outputHeight); + } + @RequiresNonNull("transformationMatrix") private void applyAspectRatio() { float inputAspectRatio = outputWidth / outputHeight; diff --git a/library/effect/src/main/java/com/google/android/exoplayer2/effect/ScaleToFitTransformation.java b/library/effect/src/main/java/com/google/android/exoplayer2/effect/ScaleToFitTransformation.java index f7a34f89b3..ae270963b9 100644 --- a/library/effect/src/main/java/com/google/android/exoplayer2/effect/ScaleToFitTransformation.java +++ b/library/effect/src/main/java/com/google/android/exoplayer2/effect/ScaleToFitTransformation.java @@ -90,13 +90,6 @@ public final class ScaleToFitTransformation implements MatrixTransformation { } } - /** The multiplier by which the frame will scale horizontally, along the x-axis. */ - public final float scaleX; - /** The multiplier by which the frame will scale vertically, along the y-axis. */ - public final float scaleY; - /** How much to rotate the frame counterclockwise, in degrees. */ - public final float rotationDegrees; - private final Matrix transformationMatrix; private @MonotonicNonNull Matrix adjustedTransformationMatrix; @@ -108,9 +101,6 @@ public final class ScaleToFitTransformation implements MatrixTransformation { * @param rotationDegrees How much to rotate the frame counterclockwise, in degrees. */ private ScaleToFitTransformation(float scaleX, float scaleY, float rotationDegrees) { - this.scaleX = scaleX; - this.scaleY = scaleY; - this.rotationDegrees = rotationDegrees; transformationMatrix = new Matrix(); transformationMatrix.postScale(scaleX, scaleY); transformationMatrix.postRotate(rotationDegrees); @@ -160,4 +150,12 @@ public final class ScaleToFitTransformation implements MatrixTransformation { public Matrix getMatrix(long presentationTimeUs) { return checkStateNotNull(adjustedTransformationMatrix, "configure must be called first"); } + + @Override + public boolean isNoOp(int inputWidth, int inputHeight) { + Size outputSize = configure(inputWidth, inputHeight); + return checkStateNotNull(adjustedTransformationMatrix).isIdentity() + && inputWidth == outputSize.getWidth() + && inputHeight == outputSize.getHeight(); + } } diff --git a/library/effect/src/test/java/com/google/android/exoplayer2/effect/CropTest.java b/library/effect/src/test/java/com/google/android/exoplayer2/effect/CropTest.java index eba38629cf..ac43d10567 100644 --- a/library/effect/src/test/java/com/google/android/exoplayer2/effect/CropTest.java +++ b/library/effect/src/test/java/com/google/android/exoplayer2/effect/CropTest.java @@ -37,7 +37,9 @@ public final class CropTest { Crop crop = new Crop(/* left= */ -1, /* right= */ 1, /* bottom= */ -1, /* top= */ 1); Size outputSize = crop.configure(inputWidth, inputHeight); + boolean isNoOp = crop.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isTrue(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @@ -53,7 +55,9 @@ public final class CropTest { Crop crop = new Crop(left, right, bottom, top); Size outputSize = crop.configure(inputWidth, inputHeight); + boolean isNoOp = crop.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); int expectedPostCropWidth = Math.round(inputWidth * (right - left) / GlUtil.LENGTH_NDC); int expectedPostCropHeight = Math.round(inputHeight * (top - bottom) / GlUtil.LENGTH_NDC); assertThat(outputSize.getWidth()).isEqualTo(expectedPostCropWidth); diff --git a/library/effect/src/test/java/com/google/android/exoplayer2/effect/PresentationTest.java b/library/effect/src/test/java/com/google/android/exoplayer2/effect/PresentationTest.java index d3a6894f73..38164b75cf 100644 --- a/library/effect/src/test/java/com/google/android/exoplayer2/effect/PresentationTest.java +++ b/library/effect/src/test/java/com/google/android/exoplayer2/effect/PresentationTest.java @@ -37,7 +37,9 @@ public final class PresentationTest { Presentation presentation = Presentation.createForHeight(C.LENGTH_UNSET); Size outputSize = presentation.configure(inputWidth, inputHeight); + boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isTrue(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @@ -50,7 +52,9 @@ public final class PresentationTest { Presentation presentation = Presentation.createForHeight(requestedHeight); Size outputSize = presentation.configure(inputWidth, inputHeight); + boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(requestedHeight * inputWidth / inputHeight); assertThat(outputSize.getHeight()).isEqualTo(requestedHeight); } @@ -64,7 +68,9 @@ public final class PresentationTest { Presentation.createForAspectRatio(aspectRatio, Presentation.LAYOUT_SCALE_TO_FIT); Size outputSize = presentation.configure(inputWidth, inputHeight); + boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(Math.round(aspectRatio * inputHeight)); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @@ -80,7 +86,9 @@ public final class PresentationTest { requestedWidth, requestedHeight, Presentation.LAYOUT_SCALE_TO_FIT); Size outputSize = presentation.configure(inputWidth, inputHeight); + boolean isNoOp = presentation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(requestedWidth); assertThat(outputSize.getHeight()).isEqualTo(requestedHeight); } diff --git a/library/effect/src/test/java/com/google/android/exoplayer2/effect/ScaleToFitTransformationTest.java b/library/effect/src/test/java/com/google/android/exoplayer2/effect/ScaleToFitTransformationTest.java index 93daefb096..7cd23bafe7 100644 --- a/library/effect/src/test/java/com/google/android/exoplayer2/effect/ScaleToFitTransformationTest.java +++ b/library/effect/src/test/java/com/google/android/exoplayer2/effect/ScaleToFitTransformationTest.java @@ -39,7 +39,9 @@ public final class ScaleToFitTransformationTest { new ScaleToFitTransformation.Builder().build(); Size outputSize = scaleToFitTransformation.configure(inputWidth, inputHeight); + boolean isNoOp = scaleToFitTransformation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isTrue(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @@ -54,7 +56,9 @@ public final class ScaleToFitTransformationTest { .build(); Size outputSize = scaleToFitTransformation.configure(inputWidth, inputHeight); + boolean isNoOp = scaleToFitTransformation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(Math.round(inputWidth * .5f)); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @@ -67,7 +71,9 @@ public final class ScaleToFitTransformationTest { new ScaleToFitTransformation.Builder().setScale(/* scaleX= */ 2f, /* scaleY= */ 1f).build(); Size outputSize = scaleToFitTransformation.configure(inputWidth, inputHeight); + boolean isNoOp = scaleToFitTransformation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth * 2); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @@ -80,7 +86,9 @@ public final class ScaleToFitTransformationTest { new ScaleToFitTransformation.Builder().setScale(/* scaleX= */ 1f, /* scaleY= */ 2f).build(); Size outputSize = scaleToFitTransformation.configure(inputWidth, inputHeight); + boolean isNoOp = scaleToFitTransformation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight * 2); } @@ -93,7 +101,9 @@ public final class ScaleToFitTransformationTest { new ScaleToFitTransformation.Builder().setRotationDegrees(90).build(); Size outputSize = scaleToFitTransformation.configure(inputWidth, inputHeight); + boolean isNoOp = scaleToFitTransformation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(inputHeight); assertThat(outputSize.getHeight()).isEqualTo(inputWidth); } @@ -107,7 +117,9 @@ public final class ScaleToFitTransformationTest { long expectedOutputWidthHeight = 247; Size outputSize = scaleToFitTransformation.configure(inputWidth, inputHeight); + boolean isNoOp = scaleToFitTransformation.isNoOp(inputWidth, inputHeight); + assertThat(isNoOp).isFalse(); assertThat(outputSize.getWidth()).isEqualTo(expectedOutputWidthHeight); assertThat(outputSize.getHeight()).isEqualTo(expectedOutputWidthHeight); } diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java index 72c31b6db9..254161fa6c 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java @@ -32,8 +32,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.effect.Presentation; -import com.google.android.exoplayer2.effect.ScaleToFitTransformation; +import com.google.android.exoplayer2.effect.GlEffect; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.mp4.SlowMotionData; import com.google.android.exoplayer2.util.Clock; @@ -42,7 +41,6 @@ import com.google.android.exoplayer2.util.DebugViewProvider; import com.google.android.exoplayer2.util.Effect; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Size; import com.google.common.collect.ImmutableList; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -565,35 +563,34 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (inputFormat.pixelWidthHeightRatio != 1f) { return true; } + if (!areVideoEffectsAllNoOp(firstEditedMediaItem.effects.videoEffects, inputFormat)) { + return true; + } + return false; + } - // TODO(b/265927935): consider generalizing this logic. - for (int i = 0; i < firstEditedMediaItem.effects.videoEffects.size(); i++) { - Effect videoEffect = firstEditedMediaItem.effects.videoEffects.get(i); - if (videoEffect instanceof Presentation) { - Presentation presentation = (Presentation) videoEffect; - // The decoder rotates encoded frames for display by inputFormat.rotationDegrees. - int decodedWidth = - (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height; - int decodedHeight = - (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width; - Size outputSize = presentation.configure(decodedWidth, decodedHeight); - if (outputSize.getWidth() != decodedWidth || outputSize.getHeight() != decodedHeight) { - return true; - } - } else if (videoEffect instanceof ScaleToFitTransformation) { - ScaleToFitTransformation scaleToFitTransformation = - (ScaleToFitTransformation) videoEffect; - if (scaleToFitTransformation.scaleX != 1f - || scaleToFitTransformation.scaleY != 1f - || scaleToFitTransformation.rotationDegrees != 0f) { - return true; - } - } else { - return true; + /** + * Returns whether all {@code videoEffects} are {@linkplain GlEffect#isNoOp(int, int) no-ops}, + * given an input {@link Format}. + */ + private boolean areVideoEffectsAllNoOp(ImmutableList videoEffects, Format inputFormat) { + int decodedWidth = + (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.width : inputFormat.height; + int decodedHeight = + (inputFormat.rotationDegrees % 180 == 0) ? inputFormat.height : inputFormat.width; + for (int i = 0; i < videoEffects.size(); i++) { + Effect videoEffect = videoEffects.get(i); + if (!(videoEffect instanceof GlEffect)) { + // We cannot confirm whether Effect instances that are not GlEffect instances are + // no-ops. + return false; + } + GlEffect glEffect = (GlEffect) videoEffect; + if (!glEffect.isNoOp(decodedWidth, decodedHeight)) { + return false; } } - - return false; + return true; } } }