Add encoder support to ShadowMediaCodecConfig

This is a step towards unifying ShadowMediaCodecConfig structure to accommodate having both ExoPlayer and Transcoding codecs configuration.

This includes:
* Adding ability to configure encoders by calling `addEncoders(CodecInfo...)`
* A new factory method that takes specific encoders and decoders CodecInfos
* A new method `addCodecs(boolean, CodecConfig, CodecInfo...) that configures codecs with specified behavior by passing a `CodecConfig`.

This CL also includes migrating Transformer tests to ShadowMediaCodecConfig.

PiperOrigin-RevId: 747390451
This commit is contained in:
shahddaghash 2025-04-14 06:28:29 -07:00 committed by Copybara-Service
parent ed56ed22fb
commit 07be60ed93
8 changed files with 164 additions and 204 deletions

View File

@ -69,6 +69,11 @@
* Leanback extension:
* Cast extension:
* Test Utilities:
* Removed `transformer.TestUtil.addAudioDecoders(String...)`,
`transformer.TestUtil.addAudioEncoders(String...)`, and
`transformer.TestUtil.addAudioEncoders(ShadowMediaCodec.CodecConfig,
String...)`. Use `ShadowMediaCodecConfig` to configure shadow encoders
and decoders instead.
* Demo app:
* Add `PlaybackSpeedPopUpButton` Composable UI element to be part of
`ExtraControls` in `demo-compose`.

View File

@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import java.nio.ByteBuffer;
import java.util.List;
import org.junit.rules.ExternalResource;
import org.robolectric.shadows.MediaCodecInfoBuilder;
import org.robolectric.shadows.ShadowMediaCodec;
@ -132,6 +133,8 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
new CodecInfo(/* codecName= */ "exotest.audio.ac3", MimeTypes.AUDIO_AC3);
public static final CodecInfo CODEC_INFO_AC4 =
new CodecInfo(/* codecName= */ "exotest.audio.ac4", MimeTypes.AUDIO_AC4);
public static final CodecInfo CODEC_INFO_AMR_NB =
new CodecInfo(/* codecName= */ "exotest.audio.amrnb", MimeTypes.AUDIO_AMR_NB);
public static final CodecInfo CODEC_INFO_E_AC3 =
new CodecInfo(/* codecName= */ "exotest.audio.eac3", MimeTypes.AUDIO_E_AC3);
public static final CodecInfo CODEC_INFO_E_AC3_JOC =
@ -173,7 +176,7 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
/**
* @deprecated Use {@link ShadowMediaCodecConfig#withAllDefaultSupportedCodecs()} instead.
*/
// TODO(b/399861060): Remove in Media3 1.8.
// TODO(b/406437316): Remove in Media3 1.8.
@Deprecated
public static ShadowMediaCodecConfig forAllSupportedMimeTypes() {
return withAllDefaultSupportedCodecs();
@ -194,7 +197,7 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
/**
* @deprecated Use {@link ShadowMediaCodecConfig#withNoDefaultSupportedCodecs()} instead.
*/
// TODO(b/399861060): Remove in Media3 1.8.
// TODO(b/406437316): Remove in Media3 1.8.
@Deprecated
public static ShadowMediaCodecConfig withNoDefaultSupportedMimeTypes() {
return withNoDefaultSupportedCodecs();
@ -205,6 +208,20 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
return new ShadowMediaCodecConfig(ImmutableSet.of());
}
/**
* Returns a {@link ShadowMediaCodecConfig} instance configured with the provided {@code decoders}
* and {@code encoders}.
*
* <p>All codecs will work as passthrough, regardless of type.
*/
public static ShadowMediaCodecConfig withCodecs(
List<CodecInfo> decoders, List<CodecInfo> encoders) {
ImmutableSet.Builder<CodecImpl> codecs = new ImmutableSet.Builder<>();
codecs.addAll(createDecoders(decoders, /* forcePassthrough= */ true));
codecs.addAll(createEncoders(encoders));
return new ShadowMediaCodecConfig(codecs.build());
}
private final ImmutableSet<CodecImpl> defaultCodecs;
private ShadowMediaCodecConfig(ImmutableSet<CodecImpl> defaultCodecs) {
@ -267,6 +284,39 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
}
}
/**
* Configures and publishes {@linkplain ShadowMediaCodec shadow encoders} based on {@code
* encoders}.
*
* <p>This method configures pass-through encoders.
*/
public void addEncoders(CodecInfo... encoders) {
for (CodecInfo encoderInfo : encoders) {
CodecImpl encoder = CodecImpl.createEncoder(encoderInfo);
encoder.configure();
}
}
/**
* Configures and publishes a {@link ShadowMediaCodec} codec.
*
* <p>Input buffers are handled according to the {@link ShadowMediaCodec.CodecConfig} provided.
*
* @param codecInfo Basic codec information.
* @param isEncoder Whether the codecs registered are encoders or decoders.
* @param codecConfig Codec configuration implementation of the shadow.
*/
public void addCodec(
CodecInfo codecInfo, boolean isEncoder, ShadowMediaCodec.CodecConfig codecConfig) {
configureShadowMediaCodec(
codecInfo.codecName,
codecInfo.mimeType,
isEncoder,
codecInfo.profileLevels,
codecInfo.colorFormats,
codecConfig);
}
@Override
protected void before() throws Throwable {
for (CodecImpl codec : this.defaultCodecs) {
@ -282,7 +332,7 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
}
private static ImmutableSet<CodecImpl> createDecoders(
ImmutableList<CodecInfo> decoderInfos, boolean forcePassthrough) {
List<CodecInfo> decoderInfos, boolean forcePassthrough) {
ImmutableSet.Builder<CodecImpl> builder = new ImmutableSet.Builder<>();
for (CodecInfo info : decoderInfos) {
if (!forcePassthrough && MimeTypes.isAudio(info.mimeType)) {
@ -294,6 +344,14 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
return builder.build();
}
private static ImmutableSet<CodecImpl> createEncoders(List<CodecInfo> encoderInfos) {
ImmutableSet.Builder<CodecImpl> builder = new ImmutableSet.Builder<>();
for (CodecInfo info : encoderInfos) {
builder.add(CodecImpl.createEncoder(info));
}
return builder.build();
}
/**
* A {@link ShadowMediaCodec.CodecConfig.Codec} that provides pass-through or frame dropping
* encoders and decoders.
@ -312,6 +370,10 @@ public final class ShadowMediaCodecConfig extends ExternalResource {
return new CodecImpl(codecInfo, /* isPassthrough= */ true, /* isEncoder= */ false);
}
public static CodecImpl createEncoder(CodecInfo codecInfo) {
return new CodecImpl(codecInfo, /* isPassthrough= */ true, /* isEncoder= */ true);
}
/**
* Creates an instance.
*

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.transformer;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_AAC;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_RAW;
import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_ONLY;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW;
@ -23,25 +25,22 @@ import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S;
import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ONLY;
import static androidx.media3.transformer.TestUtil.addAudioDecoders;
import static androidx.media3.transformer.TestUtil.addAudioEncoders;
import static androidx.media3.transformer.TestUtil.createAudioEffects;
import static androidx.media3.transformer.TestUtil.createVolumeScalingAudioProcessor;
import static androidx.media3.transformer.TestUtil.getCompositionDumpFilePath;
import static androidx.media3.transformer.TestUtil.getDumpFileName;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.TestTransformerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@ -57,15 +56,16 @@ public class CompositionExportTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
addAudioDecoders(MimeTypes.AUDIO_RAW);
addAudioEncoders(MimeTypes.AUDIO_AAC);
}
@Rule
public ShadowMediaCodecConfig shadowMediaCodecConfig =
ShadowMediaCodecConfig.withCodecs(
/* decoders= */ ImmutableList.of(CODEC_INFO_RAW),
/* encoders= */ ImmutableList.of(CODEC_INFO_AAC));
@After
public void tearDown() {
removeEncodersAndDecoders();
// TODO(b/406463016): Investigate moving this call to ShadowMediaCodecConfig#after() method.
EncoderUtil.clearCachedEncoders();
}
@Test

View File

@ -17,6 +17,9 @@
package androidx.media3.transformer;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runLooperUntil;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_AAC;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_AMR_NB;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_RAW;
import static androidx.media3.transformer.AssetLoader.SUPPORTED_OUTPUT_TYPE_DECODED;
import static androidx.media3.transformer.AssetLoader.SUPPORTED_OUTPUT_TYPE_ENCODED;
import static androidx.media3.transformer.ExportResult.CONVERSION_PROCESS_NA;
@ -36,12 +39,9 @@ import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ELST_TRIM_IDR_DURA
import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ONLY;
import static androidx.media3.transformer.TestUtil.FILE_WITH_SEF_SLOW_MOTION;
import static androidx.media3.transformer.TestUtil.FILE_WITH_SUBTITLES;
import static androidx.media3.transformer.TestUtil.addAudioDecoders;
import static androidx.media3.transformer.TestUtil.addAudioEncoders;
import static androidx.media3.transformer.TestUtil.createAudioEffects;
import static androidx.media3.transformer.TestUtil.createPitchChangingAudioProcessor;
import static androidx.media3.transformer.TestUtil.getDumpFileName;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NOT_STARTED;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE;
@ -89,6 +89,7 @@ import androidx.media3.extractor.PositionHolder;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.TestTransformerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
@ -105,7 +106,6 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@ -125,24 +125,42 @@ import org.robolectric.shadows.ShadowMediaCodec;
public final class MediaItemExportTest {
private static final long TEST_TIMEOUT_SECONDS = 10;
private static final String EXPECTED_CODEC_EXCEPTION_MESSAGE = "Unexpected format!";
private static final ShadowMediaCodec.CodecConfig THROWING_CODEC_CONFIG =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 100_000,
/* outputBufferSize= */ 100_000,
new ShadowMediaCodec.CodecConfig.Codec() {
@Override
public void process(ByteBuffer byteBuffer, ByteBuffer byteBuffer1) {
throw new IllegalStateException();
}
@Override
public void onConfigured(
MediaFormat format, Surface surface, MediaCrypto crypto, int flags) {
throw new IllegalArgumentException(EXPECTED_CODEC_EXCEPTION_MESSAGE);
}
});
@Rule public final TemporaryFolder outputDir = new TemporaryFolder();
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
addAudioDecoders(MimeTypes.AUDIO_RAW);
addAudioEncoders(MimeTypes.AUDIO_AAC);
}
@Rule
public ShadowMediaCodecConfig shadowMediaCodecConfig =
ShadowMediaCodecConfig.withCodecs(
/* decoders= */ ImmutableList.of(CODEC_INFO_RAW), /* encoders= */ ImmutableList.of());
@After
public void tearDown() {
removeEncodersAndDecoders();
// TODO(b/406463016): Investigate moving this call to ShadowMediaCodecConfig#after() method.
EncoderUtil.clearCachedEncoders();
}
@Test
public void start_gapOnlyExport_outputsSilence() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(muxerFactory).build();
@ -275,6 +293,7 @@ public final class MediaItemExportTest {
@Test
public void start_trimOptimizationEnabled_fileNotMp4_fallbackToNormalExport() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
Transformer transformer =
new TestTransformerBuilder(context)
@ -441,6 +460,7 @@ public final class MediaItemExportTest {
@Test
public void start_forceAudioTrackAndRemoveAudioWithEffects_generatesSilentAudio()
throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(muxerFactory).build();
@ -497,6 +517,7 @@ public final class MediaItemExportTest {
@Test
public void start_forceAudioTrackOnVideoOnly_generatesSilentAudio() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
Transformer transformer =
new TestTransformerBuilder(context).setMuxerFactory(muxerFactory).build();
@ -521,6 +542,7 @@ public final class MediaItemExportTest {
@Test
public void exportAudio_muxerReceivesExpectedNumberOfBytes() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
AtomicInteger bytesSeenByEffect = new AtomicInteger();
Transformer transformer =
@ -540,6 +562,7 @@ public final class MediaItemExportTest {
@Test
public void start_adjustSampleRate_completesSuccessfully() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
sonicAudioProcessor.setOutputSampleRateHz(48000);
@ -569,6 +592,7 @@ public final class MediaItemExportTest {
@Test
public void adjustAudioSpeed_toDoubleSpeed_returnsExpectedNumberOfSamples() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
sonicAudioProcessor.setSpeed(2f);
@ -599,6 +623,7 @@ public final class MediaItemExportTest {
@Test
public void start_withRawBigEndianAudioInput_completesSuccessfully() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
ToInt16PcmAudioProcessor toInt16PcmAudioProcessor = new ToInt16PcmAudioProcessor();
Transformer transformer =
@ -622,6 +647,7 @@ public final class MediaItemExportTest {
@Test
public void start_singleMediaItemAndTransmux_ignoresTransmux() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
sonicAudioProcessor.setOutputSampleRateHz(48000);
@ -702,6 +728,7 @@ public final class MediaItemExportTest {
@Test
public void start_withMultipleListeners_callsEachOnFallback() throws Exception {
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
ArgumentCaptor<Composition> compositionArgumentCaptor =
ArgumentCaptor.forClass(Composition.class);
@ -804,29 +831,11 @@ public final class MediaItemExportTest {
@Test
public void start_whenCodecFailsToConfigure_completesWithError() throws Exception {
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
String expectedFailureMessage = "Format not valid. AMR NB (3gpp)";
ShadowMediaCodec.CodecConfig throwOnConfigureCodecConfig =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 100_000,
/* outputBufferSize= */ 100_000,
/* codec= */ new ShadowMediaCodec.CodecConfig.Codec() {
@Override
public void process(ByteBuffer in, ByteBuffer out) {
out.put(in);
}
@Override
public void onConfigured(
MediaFormat format, Surface surface, MediaCrypto crypto, int flags) {
// MediaCodec#configure documented to throw IAE if format is invalid.
throw new IllegalArgumentException(expectedFailureMessage);
}
});
shadowMediaCodecConfig.addEncoders(CODEC_INFO_AAC);
// Add the AMR_NB encoder that throws when configured.
addAudioEncoders(throwOnConfigureCodecConfig, MimeTypes.AUDIO_AMR_NB);
shadowMediaCodecConfig.addCodec(
CODEC_INFO_AMR_NB, /* isEncoder= */ true, THROWING_CODEC_CONFIG);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ false);
Transformer transformer =
new TestTransformerBuilder(context)
.setMuxerFactory(muxerFactory)
@ -841,7 +850,10 @@ public final class MediaItemExportTest {
assertThat(exception.errorCode)
.isEqualTo(ExportException.ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED);
assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(exception).hasCauseThat().hasMessageThat().isEqualTo(expectedFailureMessage);
assertThat(exception)
.hasCauseThat()
.hasMessageThat()
.isEqualTo(EXPECTED_CODEC_EXCEPTION_MESSAGE);
}
@Test
@ -866,11 +878,9 @@ public final class MediaItemExportTest {
public void
start_withAudioFormatUnsupportedByMuxer_ignoresDisabledFallbackAndCompletesSuccessfully()
throws Exception {
removeEncodersAndDecoders();
addAudioDecoders(MimeTypes.AUDIO_RAW);
// RAW supported by encoder, unsupported by muxer.
// AAC supported by encoder and muxer.
addAudioEncoders(MimeTypes.AUDIO_RAW, MimeTypes.AUDIO_AAC);
shadowMediaCodecConfig.addEncoders(CODEC_INFO_RAW, CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
Transformer.Listener mockListener = mock(Transformer.Listener.class);
@ -901,11 +911,9 @@ public final class MediaItemExportTest {
@Test
public void start_withAudioFormatUnsupportedByMuxer_fallsBackAndCompletesSuccessfully()
throws Exception {
removeEncodersAndDecoders();
addAudioDecoders(MimeTypes.AUDIO_RAW);
// RAW supported by encoder, unsupported by muxer.
// AAC supported by encoder and muxer.
addAudioEncoders(MimeTypes.AUDIO_RAW, MimeTypes.AUDIO_AAC);
shadowMediaCodecConfig.addEncoders(CODEC_INFO_RAW, CODEC_INFO_AAC);
CapturingMuxer.Factory muxerFactory = new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
Transformer.Listener mockListener = mock(Transformer.Listener.class);
@ -1198,9 +1206,7 @@ public final class MediaItemExportTest {
@Test
public void analyze_audioOnlyWithItemEffect_completesSuccessfully() throws Exception {
removeEncodersAndDecoders();
addAudioDecoders(MimeTypes.AUDIO_RAW);
addThrowingAudioEncoder(MimeTypes.AUDIO_AAC);
shadowMediaCodecConfig.addCodec(CODEC_INFO_AAC, /* isEncoder= */ true, THROWING_CODEC_CONFIG);
Transformer transformer =
ExperimentalAnalyzerModeFactory.buildAnalyzer(
getApplicationContext(), new TestTransformerBuilder(getApplicationContext()).build());
@ -1220,9 +1226,7 @@ public final class MediaItemExportTest {
@Test
public void analyze_audioOnlyWithCompositionEffect_completesSuccessfully() throws Exception {
removeEncodersAndDecoders();
addAudioDecoders(MimeTypes.AUDIO_RAW);
addThrowingAudioEncoder(MimeTypes.AUDIO_AAC);
shadowMediaCodecConfig.addCodec(CODEC_INFO_AAC, /* isEncoder= */ true, THROWING_CODEC_CONFIG);
Transformer transformer =
ExperimentalAnalyzerModeFactory.buildAnalyzer(
getApplicationContext(), new TestTransformerBuilder(getApplicationContext()).build());
@ -1247,9 +1251,7 @@ public final class MediaItemExportTest {
@Test
public void analyze_audioOnly_itemAndMixerOutputMatch() throws Exception {
removeEncodersAndDecoders();
addAudioDecoders(MimeTypes.AUDIO_RAW);
addThrowingAudioEncoder(MimeTypes.AUDIO_AAC);
shadowMediaCodecConfig.addCodec(CODEC_INFO_AAC, /* isEncoder= */ true, THROWING_CODEC_CONFIG);
Transformer transformer =
ExperimentalAnalyzerModeFactory.buildAnalyzer(
getApplicationContext(), new TestTransformerBuilder(getApplicationContext()).build());
@ -1567,27 +1569,6 @@ public final class MediaItemExportTest {
/* modifications...= */ "transmuxed"));
}
private static void addThrowingAudioEncoder(String mimeType) {
ShadowMediaCodec.CodecConfig.Codec codec =
new ShadowMediaCodec.CodecConfig.Codec() {
@Override
public void process(ByteBuffer byteBuffer, ByteBuffer byteBuffer1) {
throw new IllegalStateException();
}
@Override
public void onConfigured(
MediaFormat format, Surface surface, MediaCrypto crypto, int flags) {
throw new IllegalStateException();
}
};
addAudioEncoders(
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 100_000, /* outputBufferSize= */ 100_000, codec),
mimeType);
}
private static AudioProcessor createByteCountingAudioProcessor(AtomicInteger byteCount) {
return new TeeAudioProcessor(
new TeeAudioProcessor.AudioBufferSink() {

View File

@ -16,24 +16,23 @@
package androidx.media3.transformer;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_AAC;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_RAW;
import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW_VIDEO;
import static androidx.media3.transformer.TestUtil.addAudioDecoders;
import static androidx.media3.transformer.TestUtil.addAudioEncoders;
import static androidx.media3.transformer.TestUtil.createAudioEffects;
import static androidx.media3.transformer.TestUtil.createPitchChangingAudioProcessor;
import static androidx.media3.transformer.TestUtil.getSequenceDumpFilePath;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static com.google.common.truth.Truth.assertThat;
import static java.util.stream.Collectors.toList;
import android.content.Context;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.TestTransformerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
@ -46,7 +45,6 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@ -126,6 +124,12 @@ public final class ParameterizedAudioExportTest {
@Rule public final TemporaryFolder outputDir = new TemporaryFolder();
@Rule
public ShadowMediaCodecConfig shadowMediaCodecConfig =
ShadowMediaCodecConfig.withCodecs(
/* decoders= */ ImmutableList.of(CODEC_INFO_RAW),
/* encoders= */ ImmutableList.of(CODEC_INFO_AAC));
@Parameter public SequenceConfig sequence;
private final Context context = ApplicationProvider.getApplicationContext();
@ -133,15 +137,10 @@ public final class ParameterizedAudioExportTest {
private final CapturingMuxer.Factory muxerFactory =
new CapturingMuxer.Factory(/* handleAudioAsPcm= */ true);
@Before
public void setUp() {
addAudioDecoders(MimeTypes.AUDIO_RAW);
addAudioEncoders(MimeTypes.AUDIO_AAC);
}
@After
public void tearDown() {
removeEncodersAndDecoders();
// TODO(b/406463016): Investigate moving this call to ShadowMediaCodecConfig#after() method.
EncoderUtil.clearCachedEncoders();
}
@Test

View File

@ -16,6 +16,8 @@
package androidx.media3.transformer;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_AAC;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_RAW;
import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_AMR_NB;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW;
@ -23,24 +25,20 @@ import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW_STEREO_48000KH
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_VIDEO_ONLY;
import static androidx.media3.transformer.TestUtil.addAudioDecoders;
import static androidx.media3.transformer.TestUtil.addAudioEncoders;
import static androidx.media3.transformer.TestUtil.createAudioEffects;
import static androidx.media3.transformer.TestUtil.createVolumeScalingAudioProcessor;
import static androidx.media3.transformer.TestUtil.getDumpFileName;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static org.junit.Assume.assumeFalse;
import android.content.Context;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.TestTransformerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@ -93,17 +91,18 @@ public final class ParameterizedItemExportTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
// Only add RAW decoder, so non-RAW audio has no options for decoding.
addAudioDecoders(MimeTypes.AUDIO_RAW);
// Use an AAC encoder because muxer supports AAC.
addAudioEncoders(MimeTypes.AUDIO_AAC);
}
// Only add RAW decoder, so non-RAW audio has no options for decoding.
// Use an AAC encoder because muxer supports AAC.
@Rule
public ShadowMediaCodecConfig shadowMediaCodecConfig =
ShadowMediaCodecConfig.withCodecs(
/* decoders= */ ImmutableList.of(CODEC_INFO_RAW),
/* encoders= */ ImmutableList.of(CODEC_INFO_AAC));
@After
public void tearDown() {
removeEncodersAndDecoders();
// TODO(b/406463016): Investigate moving this call to ShadowMediaCodecConfig#after() method.
EncoderUtil.clearCachedEncoders();
}
@Test

View File

@ -16,36 +16,34 @@
package androidx.media3.transformer;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_AAC;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.CODEC_INFO_RAW;
import static androidx.media3.transformer.TestUtil.ASSET_URI_PREFIX;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW_STEREO_48000KHZ;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_RAW_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO;
import static androidx.media3.transformer.TestUtil.FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S;
import static androidx.media3.transformer.TestUtil.addAudioDecoders;
import static androidx.media3.transformer.TestUtil.addAudioEncoders;
import static androidx.media3.transformer.TestUtil.createAudioEffects;
import static androidx.media3.transformer.TestUtil.createChannelCountChangingAudioProcessor;
import static androidx.media3.transformer.TestUtil.createPitchChangingAudioProcessor;
import static androidx.media3.transformer.TestUtil.getDumpFileName;
import static androidx.media3.transformer.TestUtil.getSequenceDumpFilePath;
import static androidx.media3.transformer.TestUtil.removeEncodersAndDecoders;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.effect.RgbFilter;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.TestTransformerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@ -69,15 +67,16 @@ public final class SequenceExportTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
addAudioDecoders(MimeTypes.AUDIO_RAW);
addAudioEncoders(MimeTypes.AUDIO_AAC);
}
@Rule
public ShadowMediaCodecConfig shadowMediaCodecConfig =
ShadowMediaCodecConfig.withCodecs(
/* decoders= */ ImmutableList.of(CODEC_INFO_RAW),
/* encoders= */ ImmutableList.of(CODEC_INFO_AAC));
@After
public void tearDown() {
removeEncodersAndDecoders();
// TODO(b/406463016): Investigate moving this call to ShadowMediaCodecConfig#after() method.
EncoderUtil.clearCachedEncoders();
}
@Test

View File

@ -15,20 +15,14 @@
*/
package androidx.media3.transformer;
import static androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig.configureShadowMediaCodec;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.ChannelMixingAudioProcessor;
import androidx.media3.common.audio.ChannelMixingMatrix;
import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.StringJoiner;
import org.robolectric.shadows.ShadowMediaCodec;
import org.robolectric.shadows.ShadowMediaCodecList;
/** Utility class for {@link Transformer} unit tests */
@UnstableApi
@ -142,83 +136,4 @@ public final class TestUtil {
+ "."
+ DUMP_FILE_EXTENSION;
}
/**
* Adds an audio decoder for each {@linkplain MimeTypes mime type}.
*
* <p>Input buffers are copied directly to the output.
*
* <p>When adding codecs, {@link #removeEncodersAndDecoders()} should be called in the test class
* {@link org.junit.After @After} method.
*/
public static void addAudioDecoders(String... mimeTypes) {
for (String mimeType : mimeTypes) {
addCodec(
mimeType,
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 150_000,
/* outputBufferSize= */ 150_000,
/* codec= */ (in, out) -> out.put(in)),
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ true);
}
}
/**
* Adds an audio encoder for each {@linkplain MimeTypes mime type}.
*
* <p>Input buffers are copied directly to the output.
*
* <p>When adding codecs, {@link #removeEncodersAndDecoders()} should be called in the test class
* {@link org.junit.After @After} method.
*/
public static void addAudioEncoders(String... mimeTypes) {
addAudioEncoders(
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 150_000,
/* outputBufferSize= */ 150_000,
/* codec= */ (in, out) -> out.put(in)),
mimeTypes);
}
/**
* Adds an audio encoder for each {@linkplain MimeTypes mime type}.
*
* <p>Input buffers are handled according to the {@link
* org.robolectric.shadows.ShadowMediaCodec.CodecConfig} provided.
*
* <p>When adding codecs, {@link #removeEncodersAndDecoders()} should be called in the test's
* {@link org.junit.After @After} method.
*/
public static void addAudioEncoders(
ShadowMediaCodec.CodecConfig codecConfig, String... mimeTypes) {
for (String mimeType : mimeTypes) {
addCodec(
mimeType, codecConfig, /* colorFormats= */ ImmutableList.of(), /* isDecoder= */ false);
}
}
/** Clears all cached codecs. */
public static void removeEncodersAndDecoders() {
ShadowMediaCodec.clearCodecs();
ShadowMediaCodecList.reset();
EncoderUtil.clearCachedEncoders();
}
private static void addCodec(
String mimeType,
ShadowMediaCodec.CodecConfig codecConfig,
ImmutableList<Integer> colorFormats,
boolean isDecoder) {
String codecName =
Util.formatInvariant(
isDecoder ? "exo.%s.decoder" : "exo.%s.encoder", mimeType.replace('/', '-'));
configureShadowMediaCodec(
codecName,
mimeType,
!isDecoder,
/* profileLevels= */ ImmutableList.of(),
colorFormats,
codecConfig);
}
}