Add a Frame Extractor test with rotated input

Adds a Frame extractor test that verifies decoder
respects rotation metadata from the mp4 container.

Do not rely on the MediaCodec decoder rotate the input.
Rotate via a video effect instead.

PiperOrigin-RevId: 698381282
This commit is contained in:
dancho 2024-11-20 07:17:43 -08:00 committed by Copybara-Service
parent 66e8b53b43
commit d92c9aa8a1
3 changed files with 71 additions and 0 deletions

View File

@ -20,6 +20,7 @@ import static androidx.media3.exoplayer.SeekParameters.CLOSEST_SYNC;
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_270;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -341,4 +342,32 @@ public class FrameExtractorTest {
instrumentation.runOnMainSync(frameExtractor::release);
frameExtractor = null;
}
@Test
public void extractFrame_oneFrameRotated_returnsFrameInCorrectOrientation() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri),
/* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 0);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
Bitmap actualBitmap = frame.bitmap;
Bitmap expectedBitmap =
readBitmap(
/* assetString= */ GOLDEN_ASSET_FOLDER_PATH
+ "internal_emulator_transformer_output_180_rotated_0.000.png");
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
assertThat(frame.presentationTimeMs).isEqualTo(0);
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
assertThat(
frameExtractor
.getDecoderCounters()
.get(TIMEOUT_SECONDS, SECONDS)
.renderedOutputBufferCount)
.isEqualTo(1);
}
}

View File

@ -47,9 +47,12 @@ import androidx.media3.effect.GlEffect;
import androidx.media3.effect.GlShaderProgram;
import androidx.media3.effect.MatrixTransformation;
import androidx.media3.effect.PassthroughShaderProgram;
import androidx.media3.effect.ScaleAndRotateTransformation;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DecoderReuseEvaluation;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
@ -379,6 +382,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final class FrameExtractorRenderer extends MediaCodecVideoRenderer {
private boolean frameRenderedSinceLastReset;
private List<Effect> effectsFromPlayer;
private @MonotonicNonNull Effect rotation;
public FrameExtractorRenderer(
Context context, VideoRendererEventListener videoRendererEventListener) {
@ -389,6 +394,43 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Util.createHandlerForCurrentOrMainLooper(),
videoRendererEventListener,
/* maxDroppedFramesToNotify= */ 0);
effectsFromPlayer = ImmutableList.of();
}
@Override
public void setVideoEffects(List<Effect> effects) {
effectsFromPlayer = effects;
setEffectsWithRotation();
}
@Override
@Nullable
protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder)
throws ExoPlaybackException {
if (formatHolder.format != null) {
Format format = formatHolder.format;
if (format.rotationDegrees != 0) {
// Some decoders do not apply rotation. It's no extra cost to rotate with a GL matrix
// transformation effect instead.
// https://developer.android.com/reference/android/media/MediaCodec#transformations-when-rendering-onto-surface
rotation =
new ScaleAndRotateTransformation.Builder()
.setRotationDegrees(360 - format.rotationDegrees)
.build();
setEffectsWithRotation();
formatHolder.format = format.buildUpon().setRotationDegrees(0).build();
}
}
return super.onInputFormatChanged(formatHolder);
}
private void setEffectsWithRotation() {
ImmutableList.Builder<Effect> effectBuilder = new ImmutableList.Builder<>();
if (rotation != null) {
effectBuilder.add(rotation);
}
effectBuilder.addAll(effectsFromPlayer);
super.setVideoEffects(effectBuilder.build());
}
@Override