From f40f97fa7942b0c0d4a9f5159d8c995504f9c6c2 Mon Sep 17 00:00:00 2001 From: claincly Date: Tue, 8 Mar 2022 15:31:06 +0000 Subject: [PATCH] Improve resolution fallback logic. With the new version, we try the following before fixing resolution: - Fix size alignment - Try 3/4 the width and height - Try 2/3 the width and height - Try 1/2 the width and height Also: align the resolution ends in 1 or 9 to 0. PiperOrigin-RevId: 433206358 --- .../transformer/DefaultEncoderFactory.java | 4 +- .../exoplayer2/transformer/EncoderUtil.java | 62 ++++++++++++++----- .../transformer/EncoderUtilTest.java | 36 +++++++++-- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultEncoderFactory.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultEncoderFactory.java index 78bc3b9fad..0a1c6ca5c1 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultEncoderFactory.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/DefaultEncoderFactory.java @@ -232,7 +232,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { /* cost= */ (encoderInfo) -> { @Nullable Pair closestSupportedResolution = - EncoderUtil.getClosestSupportedResolution( + EncoderUtil.getSupportedResolution( encoderInfo, mimeType, requestedFormat.width, requestedFormat.height); if (closestSupportedResolution == null) { // Drops encoder. @@ -248,7 +248,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { // The supported resolution is the same for all remaining encoders. Pair finalResolution = checkNotNull( - EncoderUtil.getClosestSupportedResolution( + EncoderUtil.getSupportedResolution( filteredEncoders.get(0), mimeType, requestedFormat.width, requestedFormat.height)); int requestedBitrate = diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/EncoderUtil.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/EncoderUtil.java index 961ec56995..661caec25f 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/EncoderUtil.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/EncoderUtil.java @@ -63,15 +63,16 @@ public final class EncoderUtil { } /** - * Finds the {@link MediaCodecInfo encoder}'s closest supported resolution from the given - * resolution. + * Finds a {@link MediaCodecInfo encoder}'s supported resolution from a given resolution. * - *

The input resolution is returned, if it is supported by the {@link MediaCodecInfo encoder}. + *

The input resolution is returned, if it (after aligning to the encoders requirement) is + * supported by the {@link MediaCodecInfo encoder}. * - *

The resolution will be clamped to the {@link MediaCodecInfo encoder}'s range of supported - * resolutions, and adjusted to the {@link MediaCodecInfo encoder}'s size alignment. The - * adjustment process takes into account the original aspect ratio. But the fixed resolution may - * not preserve the original aspect ratio, depending on the encoder's required size alignment. + *

The resolution will be adjusted to be within the {@link MediaCodecInfo encoder}'s range of + * supported resolutions, and will be aligned to the {@link MediaCodecInfo encoder}'s alignment + * requirement. The adjustment process takes into account the original aspect ratio. But the fixed + * resolution may not preserve the original aspect ratio, depending on the encoder's required size + * alignment. * * @param encoderInfo The {@link MediaCodecInfo} of the encoder. * @param mimeType The output MIME type. @@ -80,27 +81,50 @@ public final class EncoderUtil { * @return A {@link Pair} of width and height, or {@code null} if unable to find a fix. */ @Nullable - public static Pair getClosestSupportedResolution( + public static Pair getSupportedResolution( MediaCodecInfo encoderInfo, String mimeType, int width, int height) { MediaCodecInfo.VideoCapabilities videoEncoderCapabilities = encoderInfo.getCapabilitiesForType(mimeType).getVideoCapabilities(); + int widthAlignment = videoEncoderCapabilities.getWidthAlignment(); + int heightAlignment = videoEncoderCapabilities.getHeightAlignment(); + // Fix size alignment. + width = alignResolution(width, widthAlignment); + height = alignResolution(height, heightAlignment); if (videoEncoderCapabilities.isSizeSupported(width, height)) { return Pair.create(width, height); } + // Try three-fourths (e.g. 1440 -> 1080). + int newWidth = alignResolution(width * 3 / 4, widthAlignment); + int newHeight = alignResolution(height * 3 / 4, heightAlignment); + if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { + return Pair.create(newWidth, newHeight); + } + + // Try two-thirds (e.g. 4k -> 1440). + newWidth = alignResolution(width * 2 / 3, widthAlignment); + newHeight = alignResolution(height * 2 / 3, heightAlignment); + if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { + return Pair.create(newWidth, newHeight); + } + + // Try half (e.g. 4k -> 1080). + newWidth = alignResolution(width / 2, widthAlignment); + newHeight = alignResolution(height / 2, heightAlignment); + if (videoEncoderCapabilities.isSizeSupported(newWidth, newHeight)) { + return Pair.create(newWidth, newHeight); + } + // Fix frame being too wide or too tall. width = videoEncoderCapabilities.getSupportedWidths().clamp(width); int adjustedHeight = videoEncoderCapabilities.getSupportedHeightsFor(width).clamp(height); if (adjustedHeight != height) { - width = (int) round((double) width * adjustedHeight / height); - height = adjustedHeight; + width = + alignResolution((int) round((double) width * adjustedHeight / height), widthAlignment); + height = alignResolution(adjustedHeight, heightAlignment); } - // Fix pixel alignment. - width = alignResolution(width, videoEncoderCapabilities.getWidthAlignment()); - height = alignResolution(height, videoEncoderCapabilities.getHeightAlignment()); - return videoEncoderCapabilities.isSizeSupported(width, height) ? Pair.create(width, height) : null; @@ -185,7 +209,15 @@ public final class EncoderUtil { * aligned to 48. */ private static int alignResolution(int size, int alignment) { - return alignment * Math.round((float) size / alignment); + // Aligning to resolutions that are multiples of 10, like from 1081 to 1080, assuming alignment + // is 2 in most encoders. + boolean shouldRoundDown = false; + if (size % 10 == 1) { + shouldRoundDown = true; + } + return shouldRoundDown + ? (int) (alignment * Math.floor((float) size / alignment)) + : alignment * Math.round((float) size / alignment); } private static synchronized void maybePopulateEncoderInfos() { diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/EncoderUtilTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/EncoderUtilTest.java index f7ced552c4..e06d0c7627 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/EncoderUtilTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/EncoderUtilTest.java @@ -62,28 +62,54 @@ public class EncoderUtilTest { } @Test - public void getClosestSupportedResolution_withSupportedResolution_succeeds() { + public void getSupportedResolution_withSupportedResolution_succeeds() { ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); MediaCodecInfo encoderInfo = supportedEncoders.get(0); @Nullable Pair closestSupportedResolution = - EncoderUtil.getClosestSupportedResolution(encoderInfo, MIME_TYPE, 1920, 1080); + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 1920, 1080); assertThat(closestSupportedResolution).isNotNull(); assertThat(closestSupportedResolution).isEqualTo(Pair.create(1920, 1080)); } @Test - public void getClosestSupportedResolution_withWidthTooBig_findsMostCloselySupportedResolution() { + public void getSupportedResolution_withUnalignedSize_findsMostCloselySupportedResolution() { ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); MediaCodecInfo encoderInfo = supportedEncoders.get(0); @Nullable Pair closestSupportedResolution = - EncoderUtil.getClosestSupportedResolution(encoderInfo, MIME_TYPE, 1920, 1920); + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 1919, 1081); assertThat(closestSupportedResolution).isNotNull(); - assertThat(closestSupportedResolution).isEqualTo(Pair.create(1088, 1088)); + assertThat(closestSupportedResolution).isEqualTo(Pair.create(1920, 1080)); + } + + @Test + public void getSupportedResolution_withWidthTooBig_findsTwoThirdsOfTheOriginalSize() { + ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); + MediaCodecInfo encoderInfo = supportedEncoders.get(0); + + @Nullable + Pair closestSupportedResolution = + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 1920, 1920); + + assertThat(closestSupportedResolution).isNotNull(); + assertThat(closestSupportedResolution).isEqualTo(Pair.create(1440, 1440)); + } + + @Test + public void getSupportedResolution_withWidthTooBig2_findsHalfOfTheOriginalSize() { + ImmutableList supportedEncoders = EncoderUtil.getSupportedEncoders(MIME_TYPE); + MediaCodecInfo encoderInfo = supportedEncoders.get(0); + + @Nullable + Pair closestSupportedResolution = + EncoderUtil.getSupportedResolution(encoderInfo, MIME_TYPE, 3840, 2160); + + assertThat(closestSupportedResolution).isNotNull(); + assertThat(closestSupportedResolution).isEqualTo(Pair.create(1920, 1080)); } }