Add AudioEncoderSettings to Transformer

It allows clients to specify the audio encoding profile and bitrate.
This is similar to VideoEncoderSettings.

PiperOrigin-RevId: 679131963
This commit is contained in:
sheenachhabra 2024-09-26 06:57:18 -07:00 committed by Copybara-Service
parent f4eef88089
commit b6192f7a39
4 changed files with 245 additions and 1 deletions

View File

@ -34,6 +34,7 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.media.Image;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
@ -1215,6 +1216,28 @@ public final class AndroidTestUtil {
throw new AssumptionViolatedException(skipReason);
}
/**
* Assumes that the device supports encoding with the given MIME type and profile.
*
* @param mimeType The {@linkplain MimeTypes MIME type}.
* @param profile The {@linkplain MediaCodecInfo.CodecProfileLevel codec profile}.
* @throws AssumptionViolatedException If the device does have required encoder or profile.
*/
public static void assumeCanEncodeWithProfile(String mimeType, int profile) {
ImmutableList<MediaCodecInfo> supportedEncoders = EncoderUtil.getSupportedEncoders(mimeType);
if (supportedEncoders.isEmpty()) {
throw new AssumptionViolatedException("No supported encoders");
}
for (int i = 0; i < supportedEncoders.size(); i++) {
if (EncoderUtil.findSupportedEncodingProfiles(supportedEncoders.get(i), mimeType)
.contains(profile)) {
return;
}
}
throw new AssumptionViolatedException("Profile not supported");
}
/** Returns a {@link Muxer.Factory} depending upon the API level. */
public static Muxer.Factory getMuxerFactoryBasedOnApi() {
// MediaMuxer supports B-frame from API > 24.

View File

@ -15,7 +15,10 @@
*/
package androidx.media3.transformer;
import static android.media.MediaCodecInfo.CodecProfileLevel.AACObjectHE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.MediaFormatUtil.createFormatFromMediaFormat;
import static androidx.media3.common.util.Util.isRunningOnEmulator;
import static androidx.media3.test.utils.TestUtil.retrieveTrackFormat;
import static androidx.media3.transformer.AndroidTestUtil.JPG_ASSET;
@ -31,6 +34,7 @@ import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET;
import static androidx.media3.transformer.AndroidTestUtil.WAV_ASSET;
import static androidx.media3.transformer.AndroidTestUtil.WEBP_LARGE;
import static androidx.media3.transformer.AndroidTestUtil.assumeCanEncodeWithProfile;
import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported;
import static androidx.media3.transformer.AndroidTestUtil.createFrameCountingEffect;
import static androidx.media3.transformer.AndroidTestUtil.createOpenGlObjects;
@ -47,10 +51,12 @@ import static androidx.media3.transformer.ExportResult.OPTIMIZATION_FAILED_FORMA
import static androidx.media3.transformer.ExportResult.OPTIMIZATION_SUCCEEDED;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaFormat;
import android.net.Uri;
import android.opengl.EGLContext;
import android.os.Handler;
@ -71,6 +77,7 @@ import androidx.media3.common.audio.ChannelMixingAudioProcessor;
import androidx.media3.common.audio.ChannelMixingMatrix;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.audio.SpeedProvider;
import androidx.media3.common.util.CodecSpecificDataUtil;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSourceBitmapLoader;
@ -85,6 +92,7 @@ import androidx.media3.effect.RgbFilter;
import androidx.media3.effect.ScaleAndRotateTransformation;
import androidx.media3.effect.SpeedChangeEffect;
import androidx.media3.effect.TimestampWrapper;
import androidx.media3.exoplayer.MediaExtractorCompat;
import androidx.media3.exoplayer.audio.TeeAudioProcessor;
import androidx.media3.extractor.mp4.Mp4Extractor;
import androidx.media3.extractor.text.DefaultSubtitleParserFactory;
@ -96,6 +104,7 @@ import androidx.media3.transformer.AndroidTestUtil.FrameCountingByteBufferProces
import androidx.media3.transformer.AssetLoader.CompositionSettings;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.File;
@ -2167,6 +2176,80 @@ public class TransformerEndToEndTest {
assertThat(new File(result.filePath).length()).isGreaterThan(0);
}
@Test
public void export_setAudioEncodingProfile_changesProfile() throws Exception {
assumeFalse(shouldSkipDeviceForAacObjectHeProfileEncoding());
assumeCanEncodeWithProfile(MimeTypes.AUDIO_AAC, AACObjectHE);
Context context = ApplicationProvider.getApplicationContext();
Transformer transformer =
new Transformer.Builder(context)
.setEncoderFactory(
new AndroidTestUtil.ForceEncodeEncoderFactory(
new DefaultEncoderFactory.Builder(context)
.setRequestedAudioEncoderSettings(
new AudioEncoderSettings.Builder().setProfile(AACObjectHE).build())
.build()))
.build();
MediaItem mediaItem = new MediaItem.Builder().setUri(MP4_ASSET.uri).build();
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem).setRemoveVideo(true).build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, editedMediaItem);
MediaExtractorCompat mediaExtractor = new MediaExtractorCompat(context);
mediaExtractor.setDataSource(Uri.parse(result.filePath), /* offset= */ 0);
checkState(mediaExtractor.getTrackCount() == 1);
MediaFormat mediaFormat = mediaExtractor.getTrackFormat(/* trackIndex= */ 0);
Format format = createFormatFromMediaFormat(mediaFormat);
Pair<Integer, Integer> profileAndLevel = CodecSpecificDataUtil.getCodecProfileAndLevel(format);
assertThat(profileAndLevel.first).isEqualTo(AACObjectHE);
}
@Test
public void export_setAudioEncodingBitrate_configuresEncoderWithRequestedBitrate()
throws Exception {
Context context = ApplicationProvider.getApplicationContext();
int requestedBitrate = 60_000;
// The MediaMuxer is not writing the bitrate hence use the InAppMuxer.
Transformer transformer =
new Transformer.Builder(context)
.setMuxerFactory(new InAppMuxer.Factory.Builder().build())
.setEncoderFactory(
new AndroidTestUtil.ForceEncodeEncoderFactory(
new DefaultEncoderFactory.Builder(context)
.setRequestedAudioEncoderSettings(
new AudioEncoderSettings.Builder().setBitrate(requestedBitrate).build())
.build()))
.build();
MediaItem mediaItem = new MediaItem.Builder().setUri(MP4_ASSET.uri).build();
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(mediaItem).setRemoveVideo(true).build();
ExportTestResult result =
new TransformerAndroidTestRunner.Builder(context, transformer)
.build()
.run(testId, editedMediaItem);
MediaExtractorCompat mediaExtractor = new MediaExtractorCompat(context);
mediaExtractor.setDataSource(Uri.parse(result.filePath), /* offset= */ 0);
checkState(mediaExtractor.getTrackCount() == 1);
MediaFormat mediaFormat = mediaExtractor.getTrackFormat(/* trackIndex= */ 0);
Format format = createFormatFromMediaFormat(mediaFormat);
// The format contains the requested bitrate but the actual bitrate is generally different.
assertThat(format.bitrate).isEqualTo(requestedBitrate);
}
private static boolean shouldSkipDeviceForAacObjectHeProfileEncoding() {
// These devices claims to have the AACObjectHE profile but the profile never gets applied.
return (Util.SDK_INT == 27 && Ascii.equalsIgnoreCase(Util.MODEL, "cph1803"))
|| (Util.SDK_INT == 27 && Ascii.equalsIgnoreCase(Util.MODEL, "cph1909"))
|| (Util.SDK_INT == 27 && Ascii.equalsIgnoreCase(Util.MODEL, "redmi note 5"))
|| (Util.SDK_INT == 26 && isRunningOnEmulator());
}
private static AudioProcessor createSonic(float pitch) {
SonicAudioProcessor sonic = new SonicAudioProcessor();
sonic.setPitch(pitch);

View File

@ -0,0 +1,96 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.transformer;
import android.media.MediaCodecInfo;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** Represents the audio encoder settings. */
@UnstableApi
public final class AudioEncoderSettings {
/** Builds {@link AudioEncoderSettings} instances. */
public static final class Builder {
private int profile;
private int bitrate;
/** Creates a new instance. */
public Builder() {
profile = NO_VALUE;
bitrate = NO_VALUE;
}
/**
* Sets the {@link AudioEncoderSettings#profile}.
*
* <p>The default value is {@link #NO_VALUE} and the appropriate profile will be used
* automatically.
*
* <p>The requested profile must be one of the values defined in {@link
* MediaCodecInfo.CodecProfileLevel} and must be compatible with the requested {@linkplain
* Transformer.Builder#setAudioMimeType(String) audio MIME type}. When using the {@link
* DefaultEncoderFactory}, if the encoder does not support the requested profile, then it will
* be ignored to avoid any encoder configuration failures.
*
* @param profile The profile.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setProfile(int profile) {
this.profile = profile;
return this;
}
/**
* Sets the {@link AudioEncoderSettings#bitrate}.
*
* <p>The default value is {@link #NO_VALUE}.
*
* <p>The encoder may ignore the requested bitrate to improve the encoding quality.
*
* @param bitrate The bitrate in bits per second.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setBitrate(int bitrate) {
this.bitrate = bitrate;
return this;
}
/** Builds the instance. */
public AudioEncoderSettings build() {
return new AudioEncoderSettings(profile, bitrate);
}
}
/** A value for various fields to indicate that the field's value is unknown or not applicable. */
public static final int NO_VALUE = Format.NO_VALUE;
/** A default {@link AudioEncoderSettings}. */
public static final AudioEncoderSettings DEFAULT = new Builder().build();
/** The encoding profile. */
public final int profile;
/** The encoding bitrate in bits per second. */
public final int bitrate;
private AudioEncoderSettings(int profile, int bitrate) {
this.profile = profile;
this.bitrate = bitrate;
}
}

View File

@ -64,6 +64,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
private EncoderSelector videoEncoderSelector;
private VideoEncoderSettings requestedVideoEncoderSettings;
private AudioEncoderSettings requestedAudioEncoderSettings;
private boolean enableFallback;
private @C.Priority int codecPriority;
@ -72,6 +73,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
this.context = context.getApplicationContext();
videoEncoderSelector = EncoderSelector.DEFAULT;
requestedVideoEncoderSettings = VideoEncoderSettings.DEFAULT;
requestedAudioEncoderSettings = AudioEncoderSettings.DEFAULT;
enableFallback = true;
codecPriority = C.PRIORITY_PROCESSING_FOREGROUND;
}
@ -111,6 +113,20 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
return this;
}
/**
* Sets the requested {@link AudioEncoderSettings}.
*
* <p>The default value is {@link AudioEncoderSettings#DEFAULT}.
*
* <p>Values in {@code requestedAudioEncoderSettings} may be ignored to reduce failures.
*/
@CanIgnoreReturnValue
public Builder setRequestedAudioEncoderSettings(
AudioEncoderSettings requestedAudioEncoderSettings) {
this.requestedAudioEncoderSettings = requestedAudioEncoderSettings;
return this;
}
/**
* Sets whether the encoder can fallback.
*
@ -160,6 +176,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
private final Context context;
private final EncoderSelector videoEncoderSelector;
private final VideoEncoderSettings requestedVideoEncoderSettings;
private final AudioEncoderSettings requestedAudioEncoderSettings;
private final boolean enableFallback;
private final @C.Priority int codecPriority;
@ -203,6 +220,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
this.context = builder.context;
this.videoEncoderSelector = builder.videoEncoderSelector;
this.requestedVideoEncoderSettings = builder.requestedVideoEncoderSettings;
this.requestedAudioEncoderSettings = builder.requestedAudioEncoderSettings;
this.enableFallback = builder.enableFallback;
this.codecPriority = builder.codecPriority;
}
@ -221,11 +239,35 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory {
if (mediaCodecInfos.isEmpty()) {
throw createExportException(format, "No audio media codec found");
}
MediaCodecInfo selectedEncoder = mediaCodecInfos.get(0);
if (requestedAudioEncoderSettings.profile != AudioEncoderSettings.NO_VALUE) {
for (int i = 0; i < mediaCodecInfos.size(); i++) {
MediaCodecInfo encoderInfo = mediaCodecInfos.get(i);
if (EncoderUtil.findSupportedEncodingProfiles(encoderInfo, format.sampleMimeType)
.contains(requestedAudioEncoderSettings.profile)) {
selectedEncoder = encoderInfo;
if (format.sampleMimeType.equals(MimeTypes.AUDIO_AAC)) {
mediaFormat.setInteger(
MediaFormat.KEY_AAC_PROFILE, requestedAudioEncoderSettings.profile);
}
// On some devices setting only KEY_AAC_PROFILE for AAC does not work.
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, requestedAudioEncoderSettings.profile);
break;
}
}
}
if (requestedAudioEncoderSettings.bitrate != AudioEncoderSettings.NO_VALUE) {
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, requestedAudioEncoderSettings.bitrate);
}
return new DefaultCodec(
context,
format,
mediaFormat,
mediaCodecInfos.get(0).getName(),
selectedEncoder.getName(),
/* isDecoder= */ false,
/* outputSurface= */ null);
}