Support setting H.264 level

Try to follow recommendations in
https://developer.android.com/media/optimize/sharing
more closely

For maximum compatibility the H.264 level should be less than or equal to 4.1

PiperOrigin-RevId: 658755139
This commit is contained in:
dancho 2024-08-02 05:05:03 -07:00 committed by Copybara-Service
parent b09cea9e3a
commit a79b80fcee
3 changed files with 65 additions and 7 deletions

View File

@ -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<Integer, Integer> profileAndLevel = getCodecProfileAndLevel(format);
assertThat(profileAndLevel.first).isAtMost(AVCProfileHigh);
assertThat(profileAndLevel.second).isAtMost(AVCLevel41);
}
}

View File

@ -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
*
* <p>Sets H.264 level only if it wasn't set previously.
*
* <p>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.

View File

@ -137,8 +137,8 @@ public final class VideoEncoderSettings {
* <p>The value must be one of the values defined in {@link MediaCodecInfo.CodecProfileLevel},
* or {@link #NO_VALUE}.
*
* <p>Profile and level settings will be ignored when using {@link DefaultEncoderFactory} and
* encoding to H264.
* <p>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}.