Commit 7e639974 by tofunmi Committed by Tianyi Feng

Add TransformerTestBuilderFactory to make transformer testable by apps

PiperOrigin-RevId: 495821660
parent 6376f5f7
...@@ -26,6 +26,7 @@ dependencies { ...@@ -26,6 +26,7 @@ dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion implementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion
implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + ':lib-transformer')
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
......
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.transformer; package androidx.media3.test.utils;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.test.utils.DumpableFormat; import androidx.media3.common.util.UnstableApi;
import androidx.media3.test.utils.Dumper; import androidx.media3.transformer.Muxer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
...@@ -28,6 +28,7 @@ import java.util.List; ...@@ -28,6 +28,7 @@ import java.util.List;
* testing purposes) and delegates the actual muxing operations to another {@link Muxer} created * testing purposes) and delegates the actual muxing operations to another {@link Muxer} created
* using the factory provided. * using the factory provided.
*/ */
@UnstableApi
public final class TestMuxer implements Muxer, Dumper.Dumpable { public final class TestMuxer implements Muxer, Dumper.Dumpable {
private final Muxer muxer; private final Muxer muxer;
......
/*
* Copyright 2022 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.test.utils;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import androidx.media3.common.C;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.transformer.DefaultEncoderFactory;
import androidx.media3.transformer.DefaultMuxer;
import androidx.media3.transformer.Muxer;
import androidx.media3.transformer.Transformer;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Creates a {@link Transformer.Builder} setting up some of the common resources needed for testing
* {@link Transformer}.
*/
@UnstableApi
public final class TestTransformerBuilderFactory {
private final Context context;
private static @MonotonicNonNull TestMuxer testMuxer;
private long maxDelayBetweenSamplesMs;
/** Creates a new instance */
public TestTransformerBuilderFactory(Context context) {
this.context = context;
maxDelayBetweenSamplesMs = DefaultMuxer.Factory.DEFAULT_MAX_DELAY_BETWEEN_SAMPLES_MS;
}
/**
* Sets the muxer's {@linkplain Muxer#getMaxDelayBetweenSamplesMs() max delay} between samples.
*/
@CanIgnoreReturnValue
public TestTransformerBuilderFactory setMaxDelayBetweenSamplesMs(long maxDelayBetweenSamplesMs) {
this.maxDelayBetweenSamplesMs = maxDelayBetweenSamplesMs;
return this;
}
/** Returns a {@link Transformer.Builder} using the provided values or their defaults. */
@SuppressLint("VisibleForTests") // Suppresses warning on setting the clock outside of a test file
public Transformer.Builder create(boolean enableFallback) {
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
Muxer.Factory defaultMuxerFactory = new DefaultMuxer.Factory(maxDelayBetweenSamplesMs);
return new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory(defaultMuxerFactory))
.setEncoderFactory(
new DefaultEncoderFactory.Builder(context).setEnableFallback(enableFallback).build());
}
/**
* Returns the test muxer used in the {@link Transformer.Builder}.
*
* <p>This method should only be called after the transformation is completed.
*/
public TestMuxer getTestMuxer() {
return checkStateNotNull(testMuxer);
}
private static final class TestMuxerFactory implements Muxer.Factory {
private final Muxer.Factory defaultMuxerFactory;
public TestMuxerFactory(Muxer.Factory defaultMuxerFactory) {
this.defaultMuxerFactory = defaultMuxerFactory;
}
@Override
public Muxer create(String path) throws Muxer.MuxerException {
testMuxer = new TestMuxer(path, defaultMuxerFactory);
return testMuxer;
}
@Override
public Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws Muxer.MuxerException {
testMuxer = new TestMuxer("FD:" + parcelFileDescriptor.getFd(), defaultMuxerFactory);
return testMuxer;
}
@Override
public ImmutableList<String> getSupportedSampleMimeTypes(@C.TrackType int trackType) {
return defaultMuxerFactory.getSupportedSampleMimeTypes(trackType);
}
}
}
...@@ -20,6 +20,7 @@ dependencies { ...@@ -20,6 +20,7 @@ dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'org.robolectric:robolectric:' + robolectricVersion implementation 'org.robolectric:robolectric:' + robolectricVersion
implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + ':lib-transformer')
implementation project(modulePrefix + 'test-utils') implementation project(modulePrefix + 'test-utils')
} }
......
...@@ -16,10 +16,15 @@ ...@@ -16,10 +16,15 @@
package androidx.media3.test.utils.robolectric; package androidx.media3.test.utils.robolectric;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaCrypto;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.transformer.EncoderUtil;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
...@@ -30,105 +35,247 @@ import org.robolectric.shadows.ShadowMediaCodec; ...@@ -30,105 +35,247 @@ import org.robolectric.shadows.ShadowMediaCodec;
import org.robolectric.shadows.ShadowMediaCodecList; import org.robolectric.shadows.ShadowMediaCodecList;
/** /**
* A JUnit @Rule to configure Roboelectric's {@link ShadowMediaCodec}. * A JUnit @Rule to configure {@link ShadowMediaCodec} for transcoding or decoding.
* *
* <p>Registers a {@link org.robolectric.shadows.ShadowMediaCodec.CodecConfig} for each audio/video * <p>Registers {@link org.robolectric.shadows.ShadowMediaCodec.CodecConfig} instances for ExoPlayer
* MIME type known by ExoPlayer. * and Transformer tests.
*/ */
@UnstableApi @UnstableApi
public final class ShadowMediaCodecConfig extends ExternalResource { public final class ShadowMediaCodecConfig extends ExternalResource {
private static final String EXOTEST_VIDEO_AVC = "exotest.video.avc";
private static final String EXOTEST_VIDEO_MPEG2 = "exotest.video.mpeg2";
private static final String EXOTEST_VIDEO_VP9 = "exotest.video.vp9";
private static final String EXOTEST_AUDIO_AAC = "exotest.audio.aac";
private static final String EXOTEST_AUDIO_AC3 = "exotest.audio.ac3";
private static final String EXOTEST_AUDIO_AC4 = "exotest.audio.ac4";
private static final String EXOTEST_AUDIO_E_AC3 = "exotest.audio.eac3";
private static final String EXOTEST_AUDIO_E_AC3_JOC = "exotest.audio.eac3joc";
private static final String EXOTEST_AUDIO_FLAC = "exotest.audio.flac";
private static final String EXOTEST_AUDIO_MPEG = "exotest.audio.mpeg";
private static final String EXOTEST_AUDIO_MPEG_L2 = "exotest.audio.mpegl2";
private static final String EXOTEST_AUDIO_OPUS = "exotest.audio.opus";
private static final String EXOTEST_AUDIO_VORBIS = "exotest.audio.vorbis";
private static final String EXOTEST_AUDIO_RAW = "exotest.audio.raw";
private final boolean forTranscoding;
private ShadowMediaCodecConfig(boolean forTranscoding) {
this.forTranscoding = forTranscoding;
}
/** Creates an instance that configures {@link ShadowMediaCodec} for Transformer transcoding. */
public static ShadowMediaCodecConfig forTranscoding() {
return new ShadowMediaCodecConfig(/* forTranscoding= */ true);
}
/** Creates an instance that configures {@link ShadowMediaCodec} for Exoplayer decoding. */
public static ShadowMediaCodecConfig forAllSupportedMimeTypes() { public static ShadowMediaCodecConfig forAllSupportedMimeTypes() {
return new ShadowMediaCodecConfig(); return new ShadowMediaCodecConfig(/* forTranscoding= */ false);
} }
@Override @Override
protected void before() throws Throwable { protected void before() throws Throwable {
if (forTranscoding) {
addTranscodingCodecs();
} else {
addDecodingCodecs();
}
}
private void addTranscodingCodecs() {
ShadowMediaCodec.CodecConfig codecConfig =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 10_000,
/* outputBufferSize= */ 10_000,
/* codec= */ (in, out) -> out.put(in));
addTransformerCodec(MimeTypes.AUDIO_AAC, codecConfig, /* isDecoder= */ true);
addTransformerCodec(MimeTypes.AUDIO_AC3, codecConfig, /* isDecoder= */ true);
addTransformerCodec(MimeTypes.AUDIO_AMR_NB, codecConfig, /* isDecoder= */ true);
addTransformerCodec(MimeTypes.AUDIO_AAC, codecConfig, /* isDecoder= */ false);
ShadowMediaCodec.CodecConfig throwingCodecConfig =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 10_000,
/* outputBufferSize= */ 10_000,
new ShadowMediaCodec.CodecConfig.Codec() {
@Override
public void process(ByteBuffer in, ByteBuffer out) {
out.put(in);
}
@Override
public void onConfigured(
MediaFormat format,
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
throw new IllegalArgumentException("Format unsupported");
}
});
addTransformerCodec(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig, /* isDecoder= */ true);
addTransformerCodec(MimeTypes.AUDIO_AMR_NB, throwingCodecConfig, /* isDecoder= */ false);
}
private void addDecodingCodecs() {
// Video codecs // Video codecs
MediaCodecInfo.CodecProfileLevel avcProfileLevel = MediaCodecInfo.CodecProfileLevel avcProfileLevel =
createProfileLevel( createProfileLevel(
MediaCodecInfo.CodecProfileLevel.AVCProfileHigh, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
MediaCodecInfo.CodecProfileLevel.AVCLevel62); MediaCodecInfo.CodecProfileLevel.AVCLevel62);
configureCodec( addExoplayerCodec(
/* codecName= */ "exotest.video.avc", EXOTEST_VIDEO_AVC,
MimeTypes.VIDEO_H264, MimeTypes.VIDEO_H264,
generateDecodingCodecConfig(MimeTypes.VIDEO_H264),
ImmutableList.of(avcProfileLevel), ImmutableList.of(avcProfileLevel),
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)); ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
MediaCodecInfo.CodecProfileLevel mpeg2ProfileLevel = MediaCodecInfo.CodecProfileLevel mpeg2ProfileLevel =
createProfileLevel( createProfileLevel(
MediaCodecInfo.CodecProfileLevel.MPEG2ProfileMain, MediaCodecInfo.CodecProfileLevel.MPEG2ProfileMain,
MediaCodecInfo.CodecProfileLevel.MPEG2LevelML); MediaCodecInfo.CodecProfileLevel.MPEG2LevelML);
configureCodec( addExoplayerCodec(
/* codecName= */ "exotest.video.mpeg2", EXOTEST_VIDEO_MPEG2,
MimeTypes.VIDEO_MPEG2, MimeTypes.VIDEO_MPEG2,
generateDecodingCodecConfig(MimeTypes.VIDEO_MPEG2),
ImmutableList.of(mpeg2ProfileLevel), ImmutableList.of(mpeg2ProfileLevel),
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)); ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
configureCodec( addExoplayerCodec(
/* codecName= */ "exotest.video.vp9", EXOTEST_VIDEO_VP9,
MimeTypes.VIDEO_VP9, MimeTypes.VIDEO_VP9,
generateDecodingCodecConfig(MimeTypes.VIDEO_VP9),
ImmutableList.of(), ImmutableList.of(),
ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)); ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible));
// Audio codecs // Audio codecs
configureCodec("exotest.audio.aac", MimeTypes.AUDIO_AAC); addExoplayerCodec(
configureCodec("exotest.audio.ac3", MimeTypes.AUDIO_AC3); EXOTEST_AUDIO_AAC, MimeTypes.AUDIO_AAC, generateDecodingCodecConfig(MimeTypes.AUDIO_AAC));
configureCodec("exotest.audio.ac4", MimeTypes.AUDIO_AC4); addExoplayerCodec(
configureCodec("exotest.audio.eac3", MimeTypes.AUDIO_E_AC3); EXOTEST_AUDIO_AC3, MimeTypes.AUDIO_AC3, generateDecodingCodecConfig(MimeTypes.AUDIO_AC3));
configureCodec("exotest.audio.eac3joc", MimeTypes.AUDIO_E_AC3_JOC); addExoplayerCodec(
configureCodec("exotest.audio.flac", MimeTypes.AUDIO_FLAC); EXOTEST_AUDIO_AC4, MimeTypes.AUDIO_AC4, generateDecodingCodecConfig(MimeTypes.AUDIO_AC4));
configureCodec("exotest.audio.mpeg", MimeTypes.AUDIO_MPEG); addExoplayerCodec(
configureCodec("exotest.audio.mpegl2", MimeTypes.AUDIO_MPEG_L2); EXOTEST_AUDIO_E_AC3,
configureCodec("exotest.audio.opus", MimeTypes.AUDIO_OPUS); MimeTypes.AUDIO_E_AC3,
configureCodec("exotest.audio.vorbis", MimeTypes.AUDIO_VORBIS); generateDecodingCodecConfig(MimeTypes.AUDIO_E_AC3));
addExoplayerCodec(
EXOTEST_AUDIO_E_AC3_JOC,
MimeTypes.AUDIO_E_AC3_JOC,
generateDecodingCodecConfig(MimeTypes.AUDIO_E_AC3_JOC));
addExoplayerCodec(
EXOTEST_AUDIO_FLAC,
MimeTypes.AUDIO_FLAC,
generateDecodingCodecConfig(MimeTypes.AUDIO_FLAC));
addExoplayerCodec(
EXOTEST_AUDIO_MPEG,
MimeTypes.AUDIO_MPEG,
generateDecodingCodecConfig(MimeTypes.AUDIO_MPEG));
addExoplayerCodec(
EXOTEST_AUDIO_MPEG_L2,
MimeTypes.AUDIO_MPEG_L2,
generateDecodingCodecConfig(MimeTypes.AUDIO_MPEG_L2));
addExoplayerCodec(
EXOTEST_AUDIO_OPUS,
MimeTypes.AUDIO_OPUS,
generateDecodingCodecConfig(MimeTypes.AUDIO_OPUS));
addExoplayerCodec(
EXOTEST_AUDIO_VORBIS,
MimeTypes.AUDIO_VORBIS,
generateDecodingCodecConfig(MimeTypes.AUDIO_VORBIS));
// Raw audio should use a bypass mode and never need this codec. However, to easily assert // Raw audio should use a bypass mode and never need this codec. However, to easily assert
// failures of the bypass mode we want to detect when the raw audio is decoded by this class and // failures of the bypass mode we want to detect when the raw audio is decoded by this class and
// thus we need a codec to output samples. // thus we need a codec to output samples.
configureCodec("exotest.audio.raw", MimeTypes.AUDIO_RAW); addExoplayerCodec(
EXOTEST_AUDIO_RAW, MimeTypes.AUDIO_RAW, generateDecodingCodecConfig(MimeTypes.AUDIO_RAW));
} }
@Override @Override
protected void after() { protected void after() {
if (!forTranscoding) {
MediaCodecUtil.clearDecoderInfoCache(); MediaCodecUtil.clearDecoderInfoCache();
} else {
EncoderUtil.clearCachedEncoders();
}
ShadowMediaCodecList.reset(); ShadowMediaCodecList.reset();
ShadowMediaCodec.clearCodecs(); ShadowMediaCodec.clearCodecs();
} }
private void configureCodec(String codecName, String mimeType) { private ShadowMediaCodec.CodecConfig generateDecodingCodecConfig(String mimeType) {
configureCodec( // TODO: Update ShadowMediaCodec to consider the MediaFormat.KEY_MAX_INPUT_SIZE value passed
// to configure() so we don't have to specify large buffers here.
CodecImpl codec = new CodecImpl(mimeType);
return new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 100_000, /* outputBufferSize= */ 100_000, codec);
}
private void addTransformerCodec(
String mimeType, ShadowMediaCodec.CodecConfig codecConfig, boolean isDecoder) {
String codecName =
Util.formatInvariant(
isDecoder ? "transformertest.%s.decoder" : "transformertest.%s.encoder",
mimeType.replace('/', '.'));
addCodec(
codecName,
mimeType,
codecConfig,
/* profileLevels= */ ImmutableList.of(),
/* colorFormats= */ ImmutableList.of(),
isDecoder);
}
private void addExoplayerCodec(
String codecName, String mimeType, ShadowMediaCodec.CodecConfig codecConfig) {
addExoplayerCodec(
codecName, codecName,
mimeType, mimeType,
codecConfig,
/* profileLevels= */ ImmutableList.of(), /* profileLevels= */ ImmutableList.of(),
/* colorFormats= */ ImmutableList.of()); /* colorFormats= */ ImmutableList.of());
} }
private void configureCodec( private void addExoplayerCodec(
String codecName, String codecName,
String mimeType, String mimeType,
ShadowMediaCodec.CodecConfig codecConfig,
List<MediaCodecInfo.CodecProfileLevel> profileLevels, List<MediaCodecInfo.CodecProfileLevel> profileLevels,
List<Integer> colorFormats) { List<Integer> colorFormats) {
addCodec(codecName, mimeType, codecConfig, profileLevels, colorFormats, /* isDecoder= */ true);
}
private void addCodec(
String codecName,
String mimeType,
ShadowMediaCodec.CodecConfig codecConfig,
List<MediaCodecInfo.CodecProfileLevel> profileLevels,
List<Integer> colorFormats,
boolean isDecoder) {
MediaFormat mediaFormat = new MediaFormat(); MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString(MediaFormat.KEY_MIME, mimeType); mediaFormat.setString(MediaFormat.KEY_MIME, mimeType);
MediaCodecInfoBuilder.CodecCapabilitiesBuilder capabilities = MediaCodecInfoBuilder.CodecCapabilitiesBuilder capabilities =
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder().setMediaFormat(mediaFormat); MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
.setMediaFormat(mediaFormat)
.setIsEncoder(!isDecoder);
if (!profileLevels.isEmpty()) { if (!profileLevels.isEmpty()) {
capabilities.setProfileLevels(profileLevels.toArray(new MediaCodecInfo.CodecProfileLevel[0])); capabilities.setProfileLevels(profileLevels.toArray(new MediaCodecInfo.CodecProfileLevel[0]));
} }
if (!colorFormats.isEmpty()) { if (!colorFormats.isEmpty()) {
capabilities.setColorFormats(Ints.toArray(colorFormats)); capabilities.setColorFormats(Ints.toArray(colorFormats));
} }
ShadowMediaCodecList.addCodec( ShadowMediaCodecList.addCodec(
MediaCodecInfoBuilder.newBuilder() MediaCodecInfoBuilder.newBuilder()
.setName(codecName) .setName(codecName)
.setIsEncoder(!isDecoder)
.setCapabilities(capabilities.build()) .setCapabilities(capabilities.build())
.build()); .build());
// TODO: Update ShadowMediaCodec to consider the MediaFormat.KEY_MAX_INPUT_SIZE value passed
// to configure() so we don't have to specify large buffers here. if (isDecoder) {
CodecImpl codec = new CodecImpl(mimeType); ShadowMediaCodec.addDecoder(codecName, codecConfig);
ShadowMediaCodec.addDecoder( } else {
codecName, ShadowMediaCodec.addEncoder(codecName, codecConfig);
new ShadowMediaCodec.CodecConfig( }
/* inputBufferSize= */ 100_000, /* outputBufferSize= */ 100_000, codec));
} }
private static MediaCodecInfo.CodecProfileLevel createProfileLevel(int profile, int level) { private static MediaCodecInfo.CodecProfileLevel createProfileLevel(int profile, int level) {
......
...@@ -430,7 +430,7 @@ public final class Transformer { ...@@ -430,7 +430,7 @@ public final class Transformer {
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
@VisibleForTesting @VisibleForTesting
/* package */ Builder setClock(Clock clock) { public Builder setClock(Clock clock) {
this.clock = clock; this.clock = clock;
this.listeners = listeners.copy(looper, clock, (listener, flags) -> {}); this.listeners = listeners.copy(looper, clock, (listener, flags) -> {});
return this; return this;
......
...@@ -29,15 +29,11 @@ import static org.mockito.Mockito.never; ...@@ -29,15 +29,11 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
...@@ -53,14 +49,13 @@ import androidx.media3.extractor.ExtractorOutput; ...@@ -53,14 +49,13 @@ import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.PositionHolder;
import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock; import androidx.media3.test.utils.TestTransformerBuilderFactory;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -74,16 +69,17 @@ import java.util.concurrent.atomic.AtomicReference; ...@@ -74,16 +69,17 @@ import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.shadows.MediaCodecInfoBuilder;
import org.robolectric.shadows.ShadowMediaCodec;
import org.robolectric.shadows.ShadowMediaCodecList;
/** End-to-end test for {@link Transformer}. */ /** End-to-end test for {@link Transformer}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class TransformerEndToEndTest { public final class TransformerEndToEndTest {
@Rule
public final ShadowMediaCodecConfig mediaCodecConfig = ShadowMediaCodecConfig.forTranscoding();
private static final String ASSET_URI_PREFIX = "asset:///media/"; private static final String ASSET_URI_PREFIX = "asset:///media/";
private static final String FILE_VIDEO_ONLY = "mp4/sample_18byte_nclx_colr.mp4"; private static final String FILE_VIDEO_ONLY = "mp4/sample_18byte_nclx_colr.mp4";
private static final String FILE_AUDIO_VIDEO = "mp4/sample.mp4"; private static final String FILE_AUDIO_VIDEO = "mp4/sample.mp4";
...@@ -100,39 +96,39 @@ public final class TransformerEndToEndTest { ...@@ -100,39 +96,39 @@ public final class TransformerEndToEndTest {
private Context context; private Context context;
private String outputPath; private String outputPath;
private TestMuxer testMuxer;
private FakeClock clock;
private ProgressHolder progressHolder; private ProgressHolder progressHolder;
private TestTransformerBuilderFactory testTransformerBuilderFactory;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext(); context = ApplicationProvider.getApplicationContext();
outputPath = Util.createTempFile(context, "TransformerTest").getPath(); outputPath = Util.createTempFile(context, "TransformerTest").getPath();
clock = new FakeClock(/* isAutoAdvancing= */ true);
progressHolder = new ProgressHolder(); progressHolder = new ProgressHolder();
createEncodersAndDecoders(); testTransformerBuilderFactory = new TestTransformerBuilderFactory(context);
} }
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
Files.delete(Paths.get(outputPath)); Files.delete(Paths.get(outputPath));
removeEncodersAndDecoders();
} }
@Test @Test
public void startTransformation_videoOnlyPassthrough_completesSuccessfully() throws Exception { public void startTransformation_videoOnlyPassthrough_completesSuccessfully() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_VIDEO_ONLY)); DumpFileAsserts.assertOutput(
context, testTransformerBuilderFactory.getTestMuxer(), getDumpFileName(FILE_VIDEO_ONLY));
} }
@Test @Test
public void startTransformation_audioOnlyPassthrough_completesSuccessfully() throws Exception { public void startTransformation_audioOnlyPassthrough_completesSuccessfully() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_ENCODER); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_ENCODER);
...@@ -140,13 +136,16 @@ public final class TransformerEndToEndTest { ...@@ -140,13 +136,16 @@ public final class TransformerEndToEndTest {
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_ENCODER)); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_ENCODER));
} }
@Test @Test
public void startTransformation_audioOnlyTranscoding_completesSuccessfully() throws Exception { public void startTransformation_audioOnlyTranscoding_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder()
.setAudioMimeType(MimeTypes.AUDIO_AAC) // supported by encoder and muxer .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported by encoder and muxer
...@@ -158,24 +157,29 @@ public final class TransformerEndToEndTest { ...@@ -158,24 +157,29 @@ public final class TransformerEndToEndTest {
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_ENCODER + ".aac")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_ENCODER + ".aac"));
} }
@Test @Test
public void startTransformation_audioAndVideo_completesSuccessfully() throws Exception { public void startTransformation_audioAndVideo_completesSuccessfully() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); DumpFileAsserts.assertOutput(
context, testTransformerBuilderFactory.getTestMuxer(), getDumpFileName(FILE_AUDIO_VIDEO));
} }
@Test @Test
public void startTransformation_audioAndVideo_withClippingStartAtKeyFrame_completesSuccessfully() public void startTransformation_audioAndVideo_withClippingStartAtKeyFrame_completesSuccessfully()
throws Exception { throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem mediaItem =
new MediaItem.Builder() new MediaItem.Builder()
.setUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S) .setUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S)
...@@ -192,14 +196,15 @@ public final class TransformerEndToEndTest { ...@@ -192,14 +196,15 @@ public final class TransformerEndToEndTest {
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, context,
testMuxer, testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S + ".clipped")); getDumpFileName(FILE_AUDIO_VIDEO_INCREASING_TIMESTAMPS_15S + ".clipped"));
} }
@Test @Test
public void startTransformation_withSubtitles_completesSuccessfully() throws Exception { public void startTransformation_withSubtitles_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build()) new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build())
.build(); .build();
...@@ -208,13 +213,17 @@ public final class TransformerEndToEndTest { ...@@ -208,13 +213,17 @@ public final class TransformerEndToEndTest {
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SUBTITLES)); DumpFileAsserts.assertOutput(
context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_WITH_SUBTITLES));
} }
@Test @Test
public void startTransformation_successiveTransformations_completesSuccessfully() public void startTransformation_successiveTransformations_completesSuccessfully()
throws Exception { throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
// Transform first media item. // Transform first media item.
...@@ -226,12 +235,14 @@ public final class TransformerEndToEndTest { ...@@ -226,12 +235,14 @@ public final class TransformerEndToEndTest {
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); DumpFileAsserts.assertOutput(
context, testTransformerBuilderFactory.getTestMuxer(), getDumpFileName(FILE_AUDIO_VIDEO));
} }
@Test @Test
public void startTransformation_concurrentTransformations_throwsError() throws Exception { public void startTransformation_concurrentTransformations_throwsError() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
...@@ -243,33 +254,44 @@ public final class TransformerEndToEndTest { ...@@ -243,33 +254,44 @@ public final class TransformerEndToEndTest {
@Test @Test
public void startTransformation_removeAudio_completesSuccessfully() throws Exception { public void startTransformation_removeAudio_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).setRemoveAudio(true).build(); testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setRemoveAudio(true)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".noaudio")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_VIDEO + ".noaudio"));
} }
@Test @Test
public void startTransformation_removeVideo_completesSuccessfully() throws Exception { public void startTransformation_removeVideo_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).setRemoveVideo(true).build(); testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setRemoveVideo(true)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".novideo")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_VIDEO + ".novideo"));
} }
@Test @Test
public void startTransformation_silentAudio_completesSuccessfully() throws Exception { public void startTransformation_silentAudio_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.experimentalSetForceSilentAudio(true) .experimentalSetForceSilentAudio(true)
.build(); .build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
...@@ -278,7 +300,9 @@ public final class TransformerEndToEndTest { ...@@ -278,7 +300,9 @@ public final class TransformerEndToEndTest {
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".silentaudio")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_VIDEO + ".silentaudio"));
} }
@Test @Test
...@@ -286,7 +310,8 @@ public final class TransformerEndToEndTest { ...@@ -286,7 +310,8 @@ public final class TransformerEndToEndTest {
SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor(); SonicAudioProcessor sonicAudioProcessor = new SonicAudioProcessor();
sonicAudioProcessor.setOutputSampleRateHz(48000); sonicAudioProcessor.setOutputSampleRateHz(48000);
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setAudioProcessors(ImmutableList.of(sonicAudioProcessor)) .setAudioProcessors(ImmutableList.of(sonicAudioProcessor))
.build(); .build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
...@@ -295,7 +320,9 @@ public final class TransformerEndToEndTest { ...@@ -295,7 +320,9 @@ public final class TransformerEndToEndTest {
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".48000hz")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_VIDEO + ".48000hz"));
} }
@Test @Test
...@@ -304,7 +331,8 @@ public final class TransformerEndToEndTest { ...@@ -304,7 +331,8 @@ public final class TransformerEndToEndTest {
Transformer.Listener mockListener2 = mock(Transformer.Listener.class); Transformer.Listener mockListener2 = mock(Transformer.Listener.class);
Transformer.Listener mockListener3 = mock(Transformer.Listener.class); Transformer.Listener mockListener3 = mock(Transformer.Listener.class);
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.addListener(mockListener1) .addListener(mockListener1)
.addListener(mockListener2) .addListener(mockListener2)
.addListener(mockListener3) .addListener(mockListener3)
...@@ -325,7 +353,8 @@ public final class TransformerEndToEndTest { ...@@ -325,7 +353,8 @@ public final class TransformerEndToEndTest {
Transformer.Listener mockListener2 = mock(Transformer.Listener.class); Transformer.Listener mockListener2 = mock(Transformer.Listener.class);
Transformer.Listener mockListener3 = mock(Transformer.Listener.class); Transformer.Listener mockListener3 = mock(Transformer.Listener.class);
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.addListener(mockListener1) .addListener(mockListener1)
.addListener(mockListener2) .addListener(mockListener2)
.addListener(mockListener3) .addListener(mockListener3)
...@@ -352,7 +381,8 @@ public final class TransformerEndToEndTest { ...@@ -352,7 +381,8 @@ public final class TransformerEndToEndTest {
TransformationRequest fallbackTransformationRequest = TransformationRequest fallbackTransformationRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build(); new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build();
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ true) testTransformerBuilderFactory
.create(/* enableFallback= */ true)
.addListener(mockListener1) .addListener(mockListener1)
.addListener(mockListener2) .addListener(mockListener2)
.addListener(mockListener3) .addListener(mockListener3)
...@@ -377,7 +407,8 @@ public final class TransformerEndToEndTest { ...@@ -377,7 +407,8 @@ public final class TransformerEndToEndTest {
Transformer.Listener mockListener2 = mock(Transformer.Listener.class); Transformer.Listener mockListener2 = mock(Transformer.Listener.class);
Transformer.Listener mockListener3 = mock(Transformer.Listener.class); Transformer.Listener mockListener3 = mock(Transformer.Listener.class);
Transformer transformer1 = Transformer transformer1 =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.addListener(mockListener1) .addListener(mockListener1)
.addListener(mockListener2) .addListener(mockListener2)
.addListener(mockListener3) .addListener(mockListener3)
...@@ -396,7 +427,8 @@ public final class TransformerEndToEndTest { ...@@ -396,7 +427,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder().setFlattenForSlowMotion(true).build()) new TransformationRequest.Builder().setFlattenForSlowMotion(true).build())
.build(); .build();
...@@ -405,7 +437,10 @@ public final class TransformerEndToEndTest { ...@@ -405,7 +437,10 @@ public final class TransformerEndToEndTest {
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_WITH_SEF_SLOW_MOTION)); DumpFileAsserts.assertOutput(
context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_WITH_SEF_SLOW_MOTION));
} }
@Test @Test
...@@ -420,7 +455,10 @@ public final class TransformerEndToEndTest { ...@@ -420,7 +455,10 @@ public final class TransformerEndToEndTest {
} }
}; };
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).addListener(listener).build(); testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.addListener(listener)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
...@@ -436,7 +474,8 @@ public final class TransformerEndToEndTest { ...@@ -436,7 +474,8 @@ public final class TransformerEndToEndTest {
public void startTransformation_withAudioEncoderFormatUnsupported_completesWithError() public void startTransformation_withAudioEncoderFormatUnsupported_completesWithError()
throws Exception { throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder()
.setAudioMimeType( .setAudioMimeType(
...@@ -457,7 +496,8 @@ public final class TransformerEndToEndTest { ...@@ -457,7 +496,8 @@ public final class TransformerEndToEndTest {
public void startTransformation_withAudioDecoderFormatUnsupported_completesWithError() public void startTransformation_withAudioDecoderFormatUnsupported_completesWithError()
throws Exception { throws Exception {
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.setTransformationRequest( .setTransformationRequest(
new TransformationRequest.Builder() new TransformationRequest.Builder()
.setAudioMimeType(MimeTypes.AUDIO_AAC) // supported by encoder and muxer .setAudioMimeType(MimeTypes.AUDIO_AAC) // supported by encoder and muxer
...@@ -475,7 +515,8 @@ public final class TransformerEndToEndTest { ...@@ -475,7 +515,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void startTransformation_withIoError_completesWithError() throws Exception { public void startTransformation_withIoError_completesWithError() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4"); MediaItem mediaItem = MediaItem.fromUri("asset:///non-existing-path.mp4");
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
...@@ -495,14 +536,19 @@ public final class TransformerEndToEndTest { ...@@ -495,14 +536,19 @@ public final class TransformerEndToEndTest {
TransformationRequest fallbackTransformationRequest = TransformationRequest fallbackTransformationRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build(); new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build();
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).addListener(mockListener).build(); testTransformerBuilderFactory
.create(/* enableFallback= */ false)
.addListener(mockListener)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_MUXER + ".fallback")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_MUXER + ".fallback"));
verify(mockListener) verify(mockListener)
.onFallbackApplied(mediaItem, originalTransformationRequest, fallbackTransformationRequest); .onFallbackApplied(mediaItem, originalTransformationRequest, fallbackTransformationRequest);
} }
...@@ -516,14 +562,19 @@ public final class TransformerEndToEndTest { ...@@ -516,14 +562,19 @@ public final class TransformerEndToEndTest {
TransformationRequest fallbackTransformationRequest = TransformationRequest fallbackTransformationRequest =
new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build(); new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build();
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ true).addListener(mockListener).build(); testTransformerBuilderFactory
.create(/* enableFallback= */ true)
.addListener(mockListener)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput( DumpFileAsserts.assertOutput(
context, testMuxer, getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_MUXER + ".fallback")); context,
testTransformerBuilderFactory.getTestMuxer(),
getDumpFileName(FILE_AUDIO_UNSUPPORTED_BY_MUXER + ".fallback"));
verify(mockListener) verify(mockListener)
.onFallbackApplied(mediaItem, originalTransformationRequest, fallbackTransformationRequest); .onFallbackApplied(mediaItem, originalTransformationRequest, fallbackTransformationRequest);
} }
...@@ -533,11 +584,11 @@ public final class TransformerEndToEndTest { ...@@ -533,11 +584,11 @@ public final class TransformerEndToEndTest {
MediaSource.Factory mediaSourceFactory = MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory( new DefaultMediaSourceFactory(
context, new SlowExtractorsFactory(/* delayBetweenReadsMs= */ 10)); context, new SlowExtractorsFactory(/* delayBetweenReadsMs= */ 10));
Muxer.Factory muxerFactory = new TestMuxerFactory(/* maxDelayBetweenSamplesMs= */ 1);
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false) testTransformerBuilderFactory
.setMaxDelayBetweenSamplesMs(1)
.create(/* enableFallback= */ false)
.setMediaSourceFactory(mediaSourceFactory) .setMediaSourceFactory(mediaSourceFactory)
.setMuxerFactory(muxerFactory)
.build(); .build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
...@@ -551,20 +602,24 @@ public final class TransformerEndToEndTest { ...@@ -551,20 +602,24 @@ public final class TransformerEndToEndTest {
@Test @Test
public void startTransformation_withUnsetMaxDelayBetweenSamples_completesSuccessfully() public void startTransformation_withUnsetMaxDelayBetweenSamples_completesSuccessfully()
throws Exception { throws Exception {
Muxer.Factory muxerFactory = new TestMuxerFactory(/* maxDelayBetweenSamplesMs= */ C.TIME_UNSET);
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).setMuxerFactory(muxerFactory).build(); testTransformerBuilderFactory
.setMaxDelayBetweenSamplesMs(C.TIME_UNSET)
.create(/* enableFallback= */ false)
.build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); DumpFileAsserts.assertOutput(
context, testTransformerBuilderFactory.getTestMuxer(), getDumpFileName(FILE_AUDIO_VIDEO));
} }
@Test @Test
public void startTransformation_afterCancellation_completesSuccessfully() throws Exception { public void startTransformation_afterCancellation_completesSuccessfully() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
...@@ -575,7 +630,8 @@ public final class TransformerEndToEndTest { ...@@ -575,7 +630,8 @@ public final class TransformerEndToEndTest {
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer); TransformerTestRunner.runUntilCompleted(transformer);
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); DumpFileAsserts.assertOutput(
context, testTransformerBuilderFactory.getTestMuxer(), getDumpFileName(FILE_AUDIO_VIDEO));
} }
@Test @Test
...@@ -584,7 +640,7 @@ public final class TransformerEndToEndTest { ...@@ -584,7 +640,7 @@ public final class TransformerEndToEndTest {
anotherThread.start(); anotherThread.start();
Looper looper = anotherThread.getLooper(); Looper looper = anotherThread.getLooper();
Transformer transformer = Transformer transformer =
createTransformerBuilder(/* enableFallback= */ false).setLooper(looper).build(); testTransformerBuilderFactory.create(/* enableFallback= */ false).setLooper(looper).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
AtomicReference<Exception> exception = new AtomicReference<>(); AtomicReference<Exception> exception = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(1); CountDownLatch countDownLatch = new CountDownLatch(1);
...@@ -604,12 +660,14 @@ public final class TransformerEndToEndTest { ...@@ -604,12 +660,14 @@ public final class TransformerEndToEndTest {
countDownLatch.await(); countDownLatch.await();
assertThat(exception.get()).isNull(); assertThat(exception.get()).isNull();
DumpFileAsserts.assertOutput(context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO)); DumpFileAsserts.assertOutput(
context, testTransformerBuilderFactory.getTestMuxer(), getDumpFileName(FILE_AUDIO_VIDEO));
} }
@Test @Test
public void startTransformation_fromWrongThread_throwsError() throws Exception { public void startTransformation_fromWrongThread_throwsError() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_AUDIO_VIDEO);
HandlerThread anotherThread = new HandlerThread("AnotherThread"); HandlerThread anotherThread = new HandlerThread("AnotherThread");
AtomicReference<IllegalStateException> illegalStateException = new AtomicReference<>(); AtomicReference<IllegalStateException> illegalStateException = new AtomicReference<>();
...@@ -634,7 +692,8 @@ public final class TransformerEndToEndTest { ...@@ -634,7 +692,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void getProgress_knownDuration_returnsConsistentStates() throws Exception { public void getProgress_knownDuration_returnsConsistentStates() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
AtomicInteger previousProgressState = AtomicInteger previousProgressState =
new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY);
...@@ -680,7 +739,8 @@ public final class TransformerEndToEndTest { ...@@ -680,7 +739,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void getProgress_knownDuration_givesIncreasingPercentages() throws Exception { public void getProgress_knownDuration_givesIncreasingPercentages() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
List<Integer> progresses = new ArrayList<>(); List<Integer> progresses = new ArrayList<>();
Handler progressHandler = Handler progressHandler =
...@@ -715,7 +775,8 @@ public final class TransformerEndToEndTest { ...@@ -715,7 +775,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void getProgress_noCurrentTransformation_returnsNoTransformation() throws Exception { public void getProgress_noCurrentTransformation_returnsNoTransformation() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
@Transformer.ProgressState int stateBeforeTransform = transformer.getProgress(progressHolder); @Transformer.ProgressState int stateBeforeTransform = transformer.getProgress(progressHolder);
...@@ -729,7 +790,8 @@ public final class TransformerEndToEndTest { ...@@ -729,7 +790,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void getProgress_unknownDuration_returnsConsistentStates() throws Exception { public void getProgress_unknownDuration_returnsConsistentStates() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_UNKNOWN_DURATION); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_UNKNOWN_DURATION);
AtomicInteger previousProgressState = AtomicInteger previousProgressState =
new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY); new AtomicInteger(PROGRESS_STATE_WAITING_FOR_AVAILABILITY);
...@@ -772,7 +834,8 @@ public final class TransformerEndToEndTest { ...@@ -772,7 +834,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void getProgress_fromWrongThread_throwsError() throws Exception { public void getProgress_fromWrongThread_throwsError() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
HandlerThread anotherThread = new HandlerThread("AnotherThread"); HandlerThread anotherThread = new HandlerThread("AnotherThread");
AtomicReference<IllegalStateException> illegalStateException = new AtomicReference<>(); AtomicReference<IllegalStateException> illegalStateException = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(1); CountDownLatch countDownLatch = new CountDownLatch(1);
...@@ -796,7 +859,8 @@ public final class TransformerEndToEndTest { ...@@ -796,7 +859,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void cancel_afterCompletion_doesNotThrow() throws Exception { public void cancel_afterCompletion_doesNotThrow() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY); MediaItem mediaItem = MediaItem.fromUri(ASSET_URI_PREFIX + FILE_VIDEO_ONLY);
transformer.startTransformation(mediaItem, outputPath); transformer.startTransformation(mediaItem, outputPath);
...@@ -806,7 +870,8 @@ public final class TransformerEndToEndTest { ...@@ -806,7 +870,8 @@ public final class TransformerEndToEndTest {
@Test @Test
public void cancel_fromWrongThread_throwsError() throws Exception { public void cancel_fromWrongThread_throwsError() throws Exception {
Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); Transformer transformer =
testTransformerBuilderFactory.create(/* enableFallback= */ false).build();
HandlerThread anotherThread = new HandlerThread("AnotherThread"); HandlerThread anotherThread = new HandlerThread("AnotherThread");
AtomicReference<IllegalStateException> illegalStateException = new AtomicReference<>(); AtomicReference<IllegalStateException> illegalStateException = new AtomicReference<>();
CountDownLatch countDownLatch = new CountDownLatch(1); CountDownLatch countDownLatch = new CountDownLatch(1);
...@@ -828,147 +893,10 @@ public final class TransformerEndToEndTest { ...@@ -828,147 +893,10 @@ public final class TransformerEndToEndTest {
assertThat(illegalStateException.get()).isNotNull(); assertThat(illegalStateException.get()).isNotNull();
} }
private Transformer.Builder createTransformerBuilder(boolean enableFallback) {
return new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.setEncoderFactory(
new DefaultEncoderFactory.Builder(context).setEnableFallback(enableFallback).build());
}
private static void createEncodersAndDecoders() {
ShadowMediaCodec.CodecConfig codecConfig =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 10_000,
/* outputBufferSize= */ 10_000,
/* codec= */ (in, out) -> out.put(in));
addCodec(
MimeTypes.AUDIO_AAC,
codecConfig,
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ true);
addCodec(
MimeTypes.AUDIO_AC3,
codecConfig,
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ true);
addCodec(
MimeTypes.AUDIO_AMR_NB,
codecConfig,
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ true);
addCodec(
MimeTypes.AUDIO_AAC,
codecConfig,
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ false);
ShadowMediaCodec.CodecConfig throwingCodecConfig =
new ShadowMediaCodec.CodecConfig(
/* inputBufferSize= */ 10_000,
/* outputBufferSize= */ 10_000,
new ShadowMediaCodec.CodecConfig.Codec() {
@Override
public void process(ByteBuffer in, ByteBuffer out) {
out.put(in);
}
@Override
public void onConfigured(
MediaFormat format,
@Nullable Surface surface,
@Nullable MediaCrypto crypto,
int flags) {
throw new IllegalArgumentException("Format unsupported");
}
});
addCodec(
MimeTypes.AUDIO_AMR_WB,
throwingCodecConfig,
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ true);
addCodec(
MimeTypes.AUDIO_AMR_NB,
throwingCodecConfig,
/* colorFormats= */ ImmutableList.of(),
/* isDecoder= */ false);
}
private static void addCodec(
String mimeType,
ShadowMediaCodec.CodecConfig codecConfig,
List<Integer> colorFormats,
boolean isDecoder) {
String codecName =
Util.formatInvariant(
isDecoder ? "exo.%s.decoder" : "exo.%s.encoder", mimeType.replace('/', '-'));
if (isDecoder) {
ShadowMediaCodec.addDecoder(codecName, codecConfig);
} else {
ShadowMediaCodec.addEncoder(codecName, codecConfig);
}
MediaFormat mediaFormat = new MediaFormat();
mediaFormat.setString(MediaFormat.KEY_MIME, mimeType);
MediaCodecInfoBuilder.CodecCapabilitiesBuilder codecCapabilities =
MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder()
.setMediaFormat(mediaFormat)
.setIsEncoder(!isDecoder);
if (!colorFormats.isEmpty()) {
codecCapabilities.setColorFormats(Ints.toArray(colorFormats));
}
ShadowMediaCodecList.addCodec(
MediaCodecInfoBuilder.newBuilder()
.setName(codecName)
.setIsEncoder(!isDecoder)
.setCapabilities(codecCapabilities.build())
.build());
}
private static void removeEncodersAndDecoders() {
ShadowMediaCodec.clearCodecs();
ShadowMediaCodecList.reset();
EncoderUtil.clearCachedEncoders();
}
private static String getDumpFileName(String originalFileName) { private static String getDumpFileName(String originalFileName) {
return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION; return DUMP_FILE_OUTPUT_DIRECTORY + '/' + originalFileName + '.' + DUMP_FILE_EXTENSION;
} }
private final class TestMuxerFactory implements Muxer.Factory {
private final Muxer.Factory defaultMuxerFactory;
public TestMuxerFactory() {
defaultMuxerFactory = new DefaultMuxer.Factory();
}
public TestMuxerFactory(long maxDelayBetweenSamplesMs) {
defaultMuxerFactory = new DefaultMuxer.Factory(maxDelayBetweenSamplesMs);
}
@Override
public Muxer create(String path) throws Muxer.MuxerException {
testMuxer = new TestMuxer(path, defaultMuxerFactory);
return testMuxer;
}
@Override
public Muxer create(ParcelFileDescriptor parcelFileDescriptor) throws Muxer.MuxerException {
testMuxer = new TestMuxer("FD:" + parcelFileDescriptor.getFd(), defaultMuxerFactory);
return testMuxer;
}
@Override
public ImmutableList<String> getSupportedSampleMimeTypes(@C.TrackType int trackType) {
return defaultMuxerFactory.getSupportedSampleMimeTypes(trackType);
}
}
private static final class SlowExtractorsFactory implements ExtractorsFactory { private static final class SlowExtractorsFactory implements ExtractorsFactory {
private final long delayBetweenReadsMs; private final long delayBetweenReadsMs;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment