mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
f4eef88089
commit
b6192f7a39
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user