diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java index cf96703018..d6823a6195 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/ExportTest.java @@ -15,7 +15,12 @@ */ package androidx.media3.transformer.mh; +import static android.media.MediaCodecInfo.CodecProfileLevel.AVCLevel41; +import static android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.MediaFormatUtil.createFormatFromMediaFormat; import static androidx.media3.common.util.Util.SDK_INT; +import static androidx.media3.exoplayer.mediacodec.MediaCodecUtil.getCodecProfileAndLevel; import static androidx.media3.transformer.AndroidTestUtil.FORCE_TRANSCODE_VIDEO_EFFECTS; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_4K60_PORTRAIT; @@ -24,6 +29,7 @@ import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_BT2020_SDR; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_SEF; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_SEF_H265; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S; import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_PIXEL; import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported; import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; @@ -34,7 +40,9 @@ import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import android.content.Context; +import android.media.MediaFormat; import android.net.Uri; +import android.util.Pair; import androidx.media3.common.Effect; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; @@ -42,6 +50,7 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; import androidx.media3.effect.Presentation; import androidx.media3.effect.ScaleAndRotateTransformation; +import androidx.media3.exoplayer.MediaExtractorCompat; import androidx.media3.extractor.mp4.Mp4Extractor; import androidx.media3.extractor.text.DefaultSubtitleParserFactory; import androidx.media3.test.utils.FakeExtractorOutput; @@ -438,4 +447,43 @@ public class ExportTest { int inputVideoLevel = 41; assertThat((int) sps[spsLevelIndex]).isAtLeast(inputVideoLevel); } + + @Test + public void export_setEncodingProfileLevel_changesProfileAndLevel() throws Exception { + assumeTrue( + "Android encoding guidelines recommend H.264 baseline profile prior to API 25", + Util.SDK_INT >= 25); + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setEncoderFactory( + new ForceEncodeEncoderFactory( + new DefaultEncoderFactory.Builder(context) + .setRequestedVideoEncoderSettings( + new VideoEncoderSettings.Builder() + .setEncodingProfileLevel(AVCProfileHigh, AVCLevel41) + .build()) + .build())) + .build(); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S.uri) + .build(); + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(mediaItem).setRemoveAudio(true).build(); + + ExportTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, editedMediaItem); + + MediaExtractorCompat mediaExtractor = new MediaExtractorCompat(context); + mediaExtractor.setDataSource(Uri.parse(result.filePath), 0); + checkState(mediaExtractor.getTrackCount() == 1); + MediaFormat mediaFormat = mediaExtractor.getTrackFormat(0); + Format format = createFormatFromMediaFormat(mediaFormat); + Pair profileAndLevel = getCodecProfileAndLevel(format); + assertThat(profileAndLevel.first).isAtMost(AVCProfileHigh); + assertThat(profileAndLevel.second).isAtMost(AVCLevel41); + } } 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 44418fcfab..23fa462578 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -308,7 +308,9 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { if (supportedVideoEncoderSettings.profile != VideoEncoderSettings.NO_VALUE && supportedVideoEncoderSettings.level != VideoEncoderSettings.NO_VALUE - && Util.SDK_INT >= 23) { + && Util.SDK_INT >= 24) { + // For API levels below 24, setting profile and level can lead to failures in MediaCodec + // configuration. The encoder selects the profile/level when we don't set them. // Set profile and level at the same time to maximize compatibility, or the encoder will pick // the values. mediaFormat.setInteger(MediaFormat.KEY_PROFILE, supportedVideoEncoderSettings.profile); @@ -586,9 +588,11 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } /** - * Applying suggested profile/level settings from + * Applying suggested profile settings from * https://developer.android.com/media/optimize/sharing#b-frames_and_encoding_profiles * + *

Sets H.264 level only if it wasn't set previously. + * *

The adjustment is applied in-place to {@code mediaFormat}. */ private static void adjustMediaFormatForH264EncoderSettings( @@ -614,7 +618,9 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { // Use the highest supported profile. Don't configure B-frames, because it doesn't work on // some devices. mediaFormat.setInteger(MediaFormat.KEY_PROFILE, expectedEncodingProfile); - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); + if (!mediaFormat.containsKey(MediaFormat.KEY_LEVEL)) { + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); + } } } else if (Util.SDK_INT >= 26 && !deviceNeedsNoH264HighProfileWorkaround()) { int expectedEncodingProfile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; @@ -626,7 +632,9 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { // MediaFormat.KEY_LATENCY. This accommodates some limitations in the MediaMuxer in these // system versions. mediaFormat.setInteger(MediaFormat.KEY_PROFILE, expectedEncodingProfile); - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); + if (!mediaFormat.containsKey(MediaFormat.KEY_LEVEL)) { + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedEncodingLevel); + } // TODO(b/210593256): Set KEY_LATENCY to 2 to enable B-frame production after in-app muxing // is the default and it supports B-frames. mediaFormat.setInteger(MediaFormat.KEY_LATENCY, 1); @@ -640,7 +648,9 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { // Use the baseline profile for safest results, as encoding in baseline is required per // https://source.android.com/compatibility/5.0/android-5.0-cdd#5_2_video_encoding mediaFormat.setInteger(MediaFormat.KEY_PROFILE, expectedEncodingProfile); - mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedLevel); + if (!mediaFormat.containsKey(MediaFormat.KEY_LEVEL)) { + mediaFormat.setInteger(MediaFormat.KEY_LEVEL, supportedLevel); + } } // For API levels below 24, setting profile and level can lead to failures in MediaCodec // configuration. The encoder selects the profile/level when we don't set them. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java index ced51358cd..e769fe29e0 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -137,8 +137,8 @@ public final class VideoEncoderSettings { *

The value must be one of the values defined in {@link MediaCodecInfo.CodecProfileLevel}, * or {@link #NO_VALUE}. * - *

Profile and level settings will be ignored when using {@link DefaultEncoderFactory} and - * encoding to H264. + *

Profile settings will be ignored when using {@link DefaultEncoderFactory} and encoding to + * H264. * * @param encodingProfile The {@link VideoEncoderSettings#profile}. * @param encodingLevel The {@link VideoEncoderSettings#level}.