diff --git a/libraries/test_data/src/test/assets/media/mp4/long_1080p_videoonly_lowbitrate.mp4 b/libraries/test_data/src/test/assets/media/mp4/long_1080p_videoonly_lowbitrate.mp4 new file mode 100644 index 0000000000..9ec8754c4e Binary files /dev/null and b/libraries/test_data/src/test/assets/media/mp4/long_1080p_videoonly_lowbitrate.mp4 differ diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index c4c52a7692..fe6732ce3e 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -160,6 +160,17 @@ public final class AndroidTestUtil { .setCodecs("avc1.42C033") .build(); + public static final String MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING = + "asset:///media/mp4/long_1080p_videoonly_lowbitrate.mp4"; + public static final Format MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .setFrameRate(30.00f) + .setCodecs("avc1.42C028") + .build(); + /** Baseline profile level 3.0 H.264 stream, which should be supported on all devices. */ public static final String MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_URI_STRING = "asset:///media/mp4/sample_with_increasing_timestamps_320w_240h.mp4"; diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java new file mode 100644 index 0000000000..700461839e --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeSpeedTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer.mh; + +import static androidx.media3.transformer.AndroidTestUtil.MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.assumeFormatsSupported; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.Uri; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MimeTypes; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.EditedMediaItem; +import androidx.media3.transformer.ExportTestResult; +import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +/** Checks transcoding speed. */ +@RunWith(AndroidJUnit4.class) +public class TranscodeSpeedTest { + @Rule public final TestName testName = new TestName(); + + private String testId; + + @Before + public void setUpTestId() { + testId = testName.getMethodName(); + } + + @Test + public void export1920x1080_to1080p_completesWithAtLeast20Fps() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + assumeFormatsSupported( + context, + testId, + /* inputFormat= */ AndroidTestUtil.MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS_FORMAT, + /* outputFormat= */ AndroidTestUtil.MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS_FORMAT); + Transformer transformer = + new Transformer.Builder(context) + .setVideoMimeType(MimeTypes.VIDEO_H264) + .setEncoderFactory(new AndroidTestUtil.ForceEncodeEncoderFactory(context)) + .build(); + MediaItem mediaItem = + MediaItem.fromUri(Uri.parse(MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING)) + .buildUpon() + .setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder().setEndPositionMs(15_000).build()) + .build(); + EditedMediaItem editedMediaItem = + new EditedMediaItem.Builder(mediaItem).setRemoveAudio(true).build(); + + ExportTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(testId, editedMediaItem); + + assertThat(result.throughputFps).isAtLeast(20); + } +} 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 7ba4977dd2..8bbe1d5529 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java @@ -41,6 +41,7 @@ import androidx.media3.common.util.Util; import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; +import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayList; @@ -189,7 +190,8 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory { @Override public DefaultCodec createForAudioDecoding(Format format) throws ExportException { MediaFormat mediaFormat = createMediaFormatFromFormat(format); - return createCodecForMediaFormat(mediaFormat, format, /* outputSurface= */ null); + return createCodecForMediaFormat( + mediaFormat, format, /* outputSurface= */ null, /* devicePrefersSoftwareDecoder= */ false); } @SuppressLint("InlinedApi") @@ -244,11 +246,15 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory { mediaFormat.setInteger("importance", max(0, -codecPriority)); } - return createCodecForMediaFormat(mediaFormat, format, outputSurface); + return createCodecForMediaFormat( + mediaFormat, format, outputSurface, devicePrefersSoftwareDecoder(format)); } private DefaultCodec createCodecForMediaFormat( - MediaFormat mediaFormat, Format format, @Nullable Surface outputSurface) + MediaFormat mediaFormat, + Format format, + @Nullable Surface outputSurface, + boolean devicePrefersSoftwareDecoder) throws ExportException { List decoderInfos = ImmutableList.of(); checkNotNull(format.sampleMimeType); @@ -265,10 +271,21 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory { Log.e(TAG, "Error querying decoders", e); throw createExportException(format, /* reason= */ "Querying codecs failed"); } - if (decoderInfos.isEmpty()) { throw createExportException(format, /* reason= */ "No decoders for format"); } + if (devicePrefersSoftwareDecoder) { + List softwareDecoderInfos = new ArrayList<>(); + for (int i = 0; i < decoderInfos.size(); ++i) { + MediaCodecInfo mediaCodecInfo = decoderInfos.get(i); + if (!mediaCodecInfo.hardwareAccelerated) { + softwareDecoderInfos.add(mediaCodecInfo); + } + } + if (!softwareDecoderInfos.isEmpty()) { + decoderInfos = softwareDecoderInfos; + } + } List codecInitExceptions = new ArrayList<>(); DefaultCodec codec = @@ -354,6 +371,20 @@ public final class DefaultDecoderFactory implements Codec.DecoderFactory { return SDK_INT >= 29 && context.getApplicationInfo().targetSdkVersion >= 29; } + private static boolean devicePrefersSoftwareDecoder(Format format) { + // TODO: b/255953153 - Capture this corner case with refactored fallback API. + // Some devices fail to configure a 1080p hardware encoder when a 1080p hardware decoder + // was created. Fall back to using a software decoder (see b/283768701). + // During a 1080p -> 180p export, using the hardware decoder would be faster than software + // decoder (68 fps vs 45 fps). + // When transcoding 1080p to 1080p, software decoder + hardware encoder (33 fps) outperforms + // hardware decoder + software encoder (17 fps). + // Due to b/267740292 using hardware to software encoder fallback is risky. + return format.width * format.height >= 1920 * 1080 + && (Ascii.equalsIgnoreCase(Util.MODEL, "vivo 1906") + || Ascii.equalsIgnoreCase(Util.MODEL, "redmi 8")); + } + private static ExportException createExportException(Format format, String reason) { return ExportException.createForCodec( new IllegalArgumentException(reason),