diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index d1364dcde3..caf857b756 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -206,6 +206,25 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { /* outputSurface= */ null); } + /** + * Returns a {@link DefaultCodec} for video encoding. + * + *

Use {@link Builder#setRequestedVideoEncoderSettings} with {@link + * VideoEncoderSettings#bitrate} set to request for a specific encoding bitrate. Bitrate settings + * in {@link Format} are ignored when {@link VideoEncoderSettings#bitrate} or {@link + * VideoEncoderSettings#enableHighQualityTargeting} is set. + * + * @param format The {@link Format} (of the output data) used to determine the underlying encoder + * and its configuration values. {@link Format#sampleMimeType}, {@link Format#width} and + * {@link Format#height} are set to those of the desired output video format. {@link + * Format#rotationDegrees} is 0 and {@link Format#width} {@code >=} {@link Format#height}, + * therefore the video is always in landscape orientation. {@link Format#frameRate} is set to + * the output video's frame rate, if available. + * @param allowedMimeTypes The non-empty list of allowed output sample {@linkplain MimeTypes MIME + * types}. + * @return A {@link DefaultCodec} for video encoding. + * @throws TransformationException If no suitable {@link DefaultCodec} can be created. + */ @Override public Codec createForVideoEncoding(Format format, List allowedMimeTypes) throws TransformationException { @@ -246,26 +265,35 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { mimeType, encoderSupportedFormat.width, encoderSupportedFormat.height); mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, round(encoderSupportedFormat.frameRate)); - if (supportedVideoEncoderSettings.enableHighQualityTargeting) { - int bitrate = - new DeviceMappedEncoderBitrateProvider() - .getBitrate( - encoderInfo.getName(), - encoderSupportedFormat.width, - encoderSupportedFormat.height, - encoderSupportedFormat.frameRate); - encoderSupportedFormat = - encoderSupportedFormat.buildUpon().setAverageBitrate(bitrate).build(); - } else if (encoderSupportedFormat.bitrate == Format.NO_VALUE) { - int bitrate = - getSuggestedBitrate( - encoderSupportedFormat.width, - encoderSupportedFormat.height, - encoderSupportedFormat.frameRate); - encoderSupportedFormat = - encoderSupportedFormat.buildUpon().setAverageBitrate(bitrate).build(); + int finalBitrate; + if (enableFallback) { + finalBitrate = supportedVideoEncoderSettings.bitrate; + } else { + // supportedVideoEncoderSettings is identical to requestedVideoEncoderSettings. + if (supportedVideoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE) { + finalBitrate = supportedVideoEncoderSettings.bitrate; + } else if (supportedVideoEncoderSettings.enableHighQualityTargeting) { + finalBitrate = + new DeviceMappedEncoderBitrateProvider() + .getBitrate( + encoderInfo.getName(), + encoderSupportedFormat.width, + encoderSupportedFormat.height, + encoderSupportedFormat.frameRate); + } else if (encoderSupportedFormat.averageBitrate != Format.NO_VALUE) { + finalBitrate = encoderSupportedFormat.averageBitrate; + } else { + finalBitrate = + getSuggestedBitrate( + encoderSupportedFormat.width, + encoderSupportedFormat.height, + encoderSupportedFormat.frameRate); + } } + encoderSupportedFormat = + encoderSupportedFormat.buildUpon().setAverageBitrate(finalBitrate).build(); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, encoderSupportedFormat.averageBitrate); mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, supportedVideoEncoderSettings.bitrateMode); @@ -389,16 +417,23 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { requestedFormat.width, requestedFormat.height)); - int requestedBitrate = - videoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE - ? videoEncoderSettings.bitrate - : getSuggestedBitrate( - finalResolution.getWidth(), finalResolution.getHeight(), requestedFormat.frameRate); - - filteredEncoderInfos = - filterEncodersByBitrate(filteredEncoderInfos, mimeType, requestedBitrate); - if (filteredEncoderInfos.isEmpty()) { - return null; + int requestedBitrate = Format.NO_VALUE; + // Encoders are not filtered by bitrate if high quality targeting is enabled. + if (!videoEncoderSettings.enableHighQualityTargeting) { + requestedBitrate = + videoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE + ? videoEncoderSettings.bitrate + : requestedFormat.averageBitrate != Format.NO_VALUE + ? requestedFormat.averageBitrate + : getSuggestedBitrate( + finalResolution.getWidth(), + finalResolution.getHeight(), + requestedFormat.frameRate); + filteredEncoderInfos = + filterEncodersByBitrate(filteredEncoderInfos, mimeType, requestedBitrate); + if (filteredEncoderInfos.isEmpty()) { + return null; + } } filteredEncoderInfos = @@ -408,11 +443,6 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { return null; } - // TODO(b/238094555): Check encoder supports bitrate targeted by high quality. - MediaCodecInfo pickedEncoderInfo = filteredEncoderInfos.get(0); - int closestSupportedBitrate = - EncoderUtil.getSupportedBitrateRange(pickedEncoderInfo, mimeType).clamp(requestedBitrate); - VideoEncoderSettings.Builder supportedEncodingSettingBuilder = videoEncoderSettings.buildUpon(); Format.Builder encoderSupportedFormatBuilder = requestedFormat @@ -420,11 +450,23 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { .setSampleMimeType(mimeType) .setWidth(finalResolution.getWidth()) .setHeight(finalResolution.getHeight()); - - if (!videoEncoderSettings.enableHighQualityTargeting) { - supportedEncodingSettingBuilder.setBitrate(closestSupportedBitrate); - encoderSupportedFormatBuilder.setAverageBitrate(closestSupportedBitrate); + MediaCodecInfo pickedEncoderInfo = filteredEncoderInfos.get(0); + if (videoEncoderSettings.enableHighQualityTargeting) { + requestedBitrate = + new DeviceMappedEncoderBitrateProvider() + .getBitrate( + pickedEncoderInfo.getName(), + finalResolution.getWidth(), + finalResolution.getHeight(), + requestedFormat.frameRate); + // Resets the flag after getting a targeted bitrate, so that supportedEncodingSetting can have + // bitrate set. + supportedEncodingSettingBuilder.setEnableHighQualityTargeting(false); } + int closestSupportedBitrate = + EncoderUtil.getSupportedBitrateRange(pickedEncoderInfo, mimeType).clamp(requestedBitrate); + supportedEncodingSettingBuilder.setBitrate(closestSupportedBitrate); + encoderSupportedFormatBuilder.setAverageBitrate(closestSupportedBitrate); if (videoEncoderSettings.profile == VideoEncoderSettings.NO_VALUE || videoEncoderSettings.level == VideoEncoderSettings.NO_VALUE diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java index 48aa570c0f..13c429d6f1 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java @@ -131,6 +131,96 @@ public class DefaultEncoderFactoryTest { assertThat(actualVideoFormat.height).isEqualTo(1080); } + @Test + public void + createForVideoEncoding_setFormatAverageBitrateUnsetVideoEncoderSettings_configuresEncoderUsingFormatAverageBitrate() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + requestedVideoFormat = requestedVideoFormat.buildUpon().setAverageBitrate(5_000_000).build(); + + Format actualVideoFormat = + new DefaultEncoderFactory.Builder(context) + .setRequestedVideoEncoderSettings(VideoEncoderSettings.DEFAULT) + .build() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264); + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + assertThat(actualVideoFormat.averageBitrate).isEqualTo(5_000_000); + } + + @Test + public void + createForVideoEncoding_unsetFormatAverageBitrateAndUnsetVideoEncoderSettingsBitrate_configuresEncoderUsingDefaultBitrateMapping() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + Format actualVideoFormat = + new DefaultEncoderFactory.Builder(context) + .build() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264); + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + // The default behavior is to use DefaultEncoderFactory#getSuggestedBitrate. + // 1920 * 1080 * 30 * 0.07 * 2. + assertThat(actualVideoFormat.averageBitrate).isEqualTo(8_709_120); + } + + @Test + public void + createForVideoEncoding_setFormatAverageBitrateAndSetVideoEncoderSettingHighQualityTargeting_configuresEncoderUsingHighQualityTargeting() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + requestedVideoFormat = requestedVideoFormat.buildUpon().setAverageBitrate(5_000_000).build(); + Format actualVideoFormat = + new DefaultEncoderFactory.Builder(context) + .setRequestedVideoEncoderSettings( + new VideoEncoderSettings.Builder().setEnableHighQualityTargeting(true).build()) + .build() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264); + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + // DeviceMappedEncoderBitrateProvider will produce 1920 * 1080 * 30 * 1.4, but the value is + // clampped down to the encoder's maximum, 25_000_000. + assertThat(actualVideoFormat.averageBitrate).isEqualTo(25_000_000); + } + + @Test + public void + createForVideoEncoding_setFormatAverageBitrateAndVideoEncoderSettingsBitrate_configuresEncoderUsingVideoEncoderSettingsBitrate() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + requestedVideoFormat = requestedVideoFormat.buildUpon().setAverageBitrate(5_000_000).build(); + + Format actualVideoFormat = + new DefaultEncoderFactory.Builder(context) + .setRequestedVideoEncoderSettings( + new VideoEncoderSettings.Builder().setBitrate(10_000_000).build()) + .build() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)) + .getConfigurationFormat(); + + assertThat(actualVideoFormat.sampleMimeType).isEqualTo(MimeTypes.VIDEO_H264); + assertThat(actualVideoFormat.width).isEqualTo(1920); + assertThat(actualVideoFormat.height).isEqualTo(1080); + assertThat(actualVideoFormat.averageBitrate).isEqualTo(10_000_000); + } + @Config(sdk = 29) @Test public void