diff --git a/constants.gradle b/constants.gradle index ba974fd8e9..c68303d6a5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -26,7 +26,7 @@ project.ext { // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA guavaVersion = '31.0.1-android' mockitoVersion = '3.12.4' - robolectricVersion = '4.6.1' + robolectricVersion = '4.8-SNAPSHOT' // Keep this in sync with Google's internal Checker Framework version. checkerframeworkVersion = '3.13.0' checkerframeworkCompatVersion = '2.5.5' diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java index 72c3155424..3280df2da4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java @@ -68,16 +68,14 @@ public final class DefaultCodec implements Codec { * {@code null}. * @param configurationMediaFormat The {@link MediaFormat} to configure the underlying {@link * MediaCodec}. - * @param mediaCodecName The name of a specific {@link MediaCodec} to instantiate. If {@code - * null}, {@code DefaultCodec} uses {@link Format#sampleMimeType - * configurationFormat.sampleMimeType} to create the underlying {@link MediaCodec codec}. + * @param mediaCodecName The name of a specific {@link MediaCodec} to instantiate. * @param isDecoder Whether the {@code DefaultCodec} is intended as a decoder. * @param outputSurface The output {@link Surface} if the {@link MediaCodec} outputs to a surface. */ public DefaultCodec( Format configurationFormat, MediaFormat configurationMediaFormat, - @Nullable String mediaCodecName, + String mediaCodecName, boolean isDecoder, @Nullable Surface outputSurface) throws TransformationException { @@ -87,17 +85,11 @@ public final class DefaultCodec implements Codec { inputBufferIndex = C.INDEX_UNSET; outputBufferIndex = C.INDEX_UNSET; - String sampleMimeType = checkNotNull(configurationFormat.sampleMimeType); - boolean isVideo = MimeTypes.isVideo(sampleMimeType); + boolean isVideo = MimeTypes.isVideo(checkNotNull(configurationFormat.sampleMimeType)); @Nullable MediaCodec mediaCodec = null; @Nullable Surface inputSurface = null; try { - mediaCodec = - mediaCodecName != null - ? MediaCodec.createByCodecName(mediaCodecName) - : isDecoder - ? MediaCodec.createDecoderByType(sampleMimeType) - : MediaCodec.createEncoderByType(sampleMimeType); + mediaCodec = MediaCodec.createByCodecName(mediaCodecName); configureCodec(mediaCodec, configurationMediaFormat, isDecoder, outputSurface); if (isVideo && !isDecoder) { inputSurface = mediaCodec.createInputSurface(); @@ -108,7 +100,6 @@ public final class DefaultCodec implements Codec { inputSurface.release(); } if (mediaCodec != null) { - mediaCodecName = mediaCodec.getName(); mediaCodec.release(); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java index 5e951a6847..a044351682 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java @@ -21,11 +21,15 @@ import static androidx.media3.common.util.Util.SDK_INT; import android.media.MediaFormat; import android.view.Surface; +import androidx.annotation.Nullable; import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.MediaFormatUtil; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A default implementation of {@link Codec.DecoderFactory}. */ /* package */ final class DefaultDecoderFactory implements Codec.DecoderFactory { + @Override public Codec createForAudioDecoding(Format format) throws TransformationException { MediaFormat mediaFormat = @@ -35,12 +39,13 @@ import androidx.media3.common.util.MediaFormatUtil; mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + @Nullable + String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true); + if (mediaCodecName == null) { + throw createTransformationException(format); + } return new DefaultCodec( - format, - mediaFormat, - /* mediaCodecName= */ null, - /* isDecoder= */ true, - /* outputSurface= */ null); + format, mediaFormat, mediaCodecName, /* isDecoder= */ true, /* outputSurface= */ null); } @Override @@ -59,7 +64,23 @@ import androidx.media3.common.util.MediaFormatUtil; mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); } + @Nullable + String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true); + if (mediaCodecName == null) { + throw createTransformationException(format); + } return new DefaultCodec( - format, mediaFormat, /* mediaCodecName= */ null, /* isDecoder= */ true, outputSurface); + format, mediaFormat, mediaCodecName, /* isDecoder= */ true, outputSurface); + } + + @RequiresNonNull("#1.sampleMimeType") + private static TransformationException createTransformationException(Format format) { + return TransformationException.createForCodec( + new IllegalArgumentException("The requested decoding format is not supported."), + MimeTypes.isVideo(format.sampleMimeType), + /* isDecoder= */ true, + format, + /* mediaCodecName= */ null, + TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index 5e31a79147..b31edd8249 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -74,19 +74,14 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { throws TransformationException { // TODO(b/210591626) Add encoder selection for audio. checkArgument(!allowedMimeTypes.isEmpty()); + checkNotNull(format.sampleMimeType); if (!allowedMimeTypes.contains(format.sampleMimeType)) { if (enableFallback) { // TODO(b/210591626): Pick fallback MIME type using same strategy as for encoder // capabilities limitations. format = format.buildUpon().setSampleMimeType(allowedMimeTypes.get(0)).build(); } else { - throw TransformationException.createForCodec( - new IllegalArgumentException("The requested output format is not supported."), - /* isVideo= */ false, - /* isDecoder= */ false, - format, - /* mediaCodecName= */ null, - TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + throw createTransformationException(format); } } MediaFormat mediaFormat = @@ -94,12 +89,13 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { checkNotNull(format.sampleMimeType), format.sampleRate, format.channelCount); mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, format.bitrate); + @Nullable + String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ false); + if (mediaCodecName == null) { + throw createTransformationException(format); + } return new DefaultCodec( - format, - mediaFormat, - /* mediaCodecName= */ null, - /* isDecoder= */ false, - /* outputSurface= */ null); + format, mediaFormat, mediaCodecName, /* isDecoder= */ false, /* outputSurface= */ null); } @Override @@ -120,13 +116,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { findEncoderWithClosestFormatSupport( format, videoEncoderSelector, allowedMimeTypes, enableFallback); if (encoderAndClosestFormatSupport == null) { - throw TransformationException.createForCodec( - new IllegalArgumentException("The requested output format is not supported."), - /* isVideo= */ true, - /* isDecoder= */ false, - format, - /* mediaCodecName= */ null, - TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + throw createTransformationException(format); } MediaCodecInfo encoderInfo = encoderAndClosestFormatSupport.first; @@ -371,4 +361,15 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { // 1080p30 -> 6.2Mbps, 720p30 -> 2.7Mbps. return (int) (width * height * frameRate * 0.1); } + + @RequiresNonNull("#1.sampleMimeType") + private static TransformationException createTransformationException(Format format) { + return TransformationException.createForCodec( + new IllegalArgumentException("The requested encoding format is not supported."), + MimeTypes.isVideo(format.sampleMimeType), + /* isDecoder= */ false, + format, + /* mediaCodecName= */ null, + TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java index f9fb48b844..1fee2a8fae 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java @@ -22,12 +22,14 @@ import static java.lang.Math.round; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; +import android.media.MediaFormat; import android.util.Size; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.MediaFormatUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.base.Ascii; @@ -154,6 +156,32 @@ public final class EncoderUtil { return maxSupportedLevel; } + /** + * Finds a {@link MediaCodec codec} that supports the {@link MediaFormat}, or {@code null} if none + * is found. + */ + @Nullable + public static String findCodecForFormat(MediaFormat format, boolean isDecoder) { + MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + // Format must not include KEY_FRAME_RATE on API21. + // https://developer.android.com/reference/android/media/MediaCodecList#findDecoderForFormat(android.media.MediaFormat) + @Nullable String frameRate = null; + if (Util.SDK_INT == 21 && format.containsKey(MediaFormat.KEY_FRAME_RATE)) { + frameRate = format.getString(MediaFormat.KEY_FRAME_RATE); + format.setString(MediaFormat.KEY_FRAME_RATE, null); + } + + String mediaCodecName = + isDecoder + ? mediaCodecList.findDecoderForFormat(format) + : mediaCodecList.findEncoderForFormat(format); + + if (Util.SDK_INT == 21) { + MediaFormatUtil.maybeSetString(format, MediaFormat.KEY_FRAME_RATE, frameRate); + } + return mediaCodecName; + } + /** * Finds the {@link MediaCodecInfo encoder}'s closest supported bitrate from the given bitrate. */ diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java index 000f8c83ed..ef47ce3433 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.media.MediaCodecInfo; import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Handler; @@ -49,6 +50,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Files; @@ -64,7 +66,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; 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}. */ @RunWith(AndroidJUnit4.class) @@ -750,10 +754,26 @@ public final class TransformerEndToEndTest { /* inputBufferSize= */ 10_000, /* outputBufferSize= */ 10_000, /* codec= */ (in, out) -> out.put(in)); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AAC, codecConfig); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AC3, codecConfig); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_NB, codecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AAC, codecConfig); + 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( @@ -776,9 +796,54 @@ public final class TransformerEndToEndTest { } }); - ShadowMediaCodec.addDecoder(MimeTypes.AUDIO_AMR_WB, throwingCodecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.AUDIO_AMR_NB, throwingCodecConfig); - ShadowMediaCodec.addEncoder(MimeTypes.VIDEO_H263, throwingCodecConfig); + addCodec( + MimeTypes.AUDIO_AMR_WB, + throwingCodecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ true); + addCodec( + MimeTypes.VIDEO_H263, + throwingCodecConfig, + ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible), + /* isDecoder= */ false); + addCodec( + MimeTypes.AUDIO_AMR_NB, + throwingCodecConfig, + /* colorFormats= */ ImmutableList.of(), + /* isDecoder= */ false); + } + + private static void addCodec( + String mimeType, + ShadowMediaCodec.CodecConfig codecConfig, + List 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() {