diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/SsimMapperTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/SsimMapperTest.java new file mode 100644 index 0000000000..4247193a45 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/SsimMapperTest.java @@ -0,0 +1,276 @@ +/* + * 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.transformer.mh.analysis; + +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G; +import static androidx.media3.transformer.AndroidTestUtil.getFormatForTestFile; +import static androidx.media3.transformer.AndroidTestUtil.skipAndLogIfInsufficientCodecSupport; +import static androidx.media3.transformer.TransformationTestResult.SSIM_UNSET; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; +import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Finds the bitrate mapping for a given SSIM value. + * + *
SSIM increases monotonically with bitrate.
+ */
+@RunWith(Parameterized.class)
+public class SsimMapperTest {
+
+ // When running this test, input file list should be restricted more than this. Binary search can
+ // take up to 40 minutes to complete for a single clip on lower end devices.
+ private static final ImmutableList Performs a binary search of the bitrate between the {@link #bitrateLowerBound} and {@link
+ * #bitrateUpperBound}.
+ *
+ * Runs until the target SSIM is found or the maximum number of transformations is reached.
+ */
+ public void search() throws Exception {
+ if (!setupBinarySearchBounds()) {
+ return;
+ }
+
+ while (transformationsLeft > 0) {
+ // At this point, we have under and over bitrate bounds, with associated SSIMs.
+ // Go between the two, and replace either the under or the over.
+
+ int currentBitrate = (bitrateUpperBound + bitrateLowerBound) / 2;
+ double currentSsim = transformAndGetSsim(currentBitrate);
+ if (isSsimAcceptable(currentSsim)) {
+ return;
+ }
+
+ if (currentSsim < SSIM_TARGET) {
+ checkState(currentSsim >= ssimLowerBound, "SSIM has decreased with a higher bitrate.");
+ bitrateLowerBound = currentBitrate;
+ ssimLowerBound = currentSsim;
+ } else if (currentSsim > SSIM_TARGET) {
+ checkState(currentSsim <= ssimUpperBound, "SSIM has increased with a lower bitrate.");
+ bitrateUpperBound = currentBitrate;
+ ssimUpperBound = currentSsim;
+ } else {
+ throw new IllegalStateException(
+ "Impossible - SSIM is not above, below, or matching target.");
+ }
+ }
+ }
+
+ private double transformAndGetSsim(int bitrate) throws Exception {
+ // TODO(b/238094555): Force specific encoders to be used.
+
+ String fileName = checkNotNull(Iterables.getLast(Splitter.on("/").split(videoUri)));
+ String testId = String.format("ssim_search_%s_VBR_%s", bitrate, fileName);
+
+ Map Acceptable is defined as {@code ssim >= ssimTarget && ssim < ssimTarget +
+ * positiveTolerance}, where {@code ssimTarget} is {@link #SSIM_TARGET} and {@code
+ * positiveTolerance} is {@link #SSIM_ACCEPTABLE_TOLERANCE}.
+ */
+ private static boolean isSsimAcceptable(double ssim) {
+ double ssimDifference = ssim - SsimBinarySearcher.SSIM_TARGET;
+ return (0 <= ssimDifference)
+ && (ssimDifference < SsimBinarySearcher.SSIM_ACCEPTABLE_TOLERANCE);
+ }
+ }
+}