mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Add pixel test for replaying
I could've add another test that seeks into the media before replying, but I don't think fundamentally it's different from the one added. I wish I could add one that replays while playing, but it'd be hard to match the frames perfectly. I'll add more timestamp based tests PiperOrigin-RevId: 742229436
This commit is contained in:
parent
ff6537d69b
commit
9254efd8da
Binary file not shown.
After Width: | Height: | Size: 509 KiB |
Binary file not shown.
After Width: | Height: | Size: 233 KiB |
@ -43,10 +43,12 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.Effect;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.VideoFrameProcessor;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.effect.Brightness;
|
||||
import androidx.media3.effect.RgbMatrix;
|
||||
import androidx.media3.effect.TimestampWrapper;
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
@ -54,6 +56,8 @@ import androidx.media3.exoplayer.Renderer;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
|
||||
import androidx.media3.exoplayer.video.PlaybackVideoGraphWrapper;
|
||||
import androidx.media3.exoplayer.video.VideoFrameReleaseControl;
|
||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||
import androidx.media3.test.utils.BitmapPixelTestUtil;
|
||||
import androidx.media3.transformer.SurfaceTestActivity;
|
||||
@ -271,6 +275,147 @@ public class EffectPlaybackPixelTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exoplayerEffectRedraw_changeEffectOnFirstFrame_ensuresCorrectFramesAreRedrawn()
|
||||
throws Exception {
|
||||
// Internal reference: b/264252759.
|
||||
assumeTrue(
|
||||
"This test should run on real devices because OpenGL to ImageReader rendering is"
|
||||
+ "not always reliable on emulators.",
|
||||
!Util.isRunningOnEmulator());
|
||||
|
||||
ArrayList<BitmapPixelTestUtil.ImageBuffer> readImageBuffers = new ArrayList<>();
|
||||
AtomicInteger renderedFramesCount = new AtomicInteger();
|
||||
AtomicInteger firstFrameRenderedCount = new AtomicInteger();
|
||||
ConditionVariable playerEnded = new ConditionVariable();
|
||||
ConditionVariable readAllOutputFrames = new ConditionVariable();
|
||||
Handler mainHandler = new Handler(instrumentation.getTargetContext().getMainLooper());
|
||||
|
||||
instrumentation.runOnMainSync(
|
||||
() -> {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
Renderer videoRenderer = new ReplayVideoRenderer(context, MediaCodecSelector.DEFAULT);
|
||||
player =
|
||||
new ExoPlayer.Builder(context)
|
||||
.setRenderersFactory(
|
||||
new DefaultRenderersFactory(context) {
|
||||
@Override
|
||||
protected void buildVideoRenderers(
|
||||
Context context,
|
||||
@ExtensionRendererMode int extensionRendererMode,
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
boolean enableDecoderFallback,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
ArrayList<Renderer> builtVideoRenderers) {
|
||||
builtVideoRenderers.add(videoRenderer);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
checkStateNotNull(outputImageReader);
|
||||
outputImageReader.setOnImageAvailableListener(
|
||||
imageReader -> {
|
||||
try (Image image = imageReader.acquireNextImage()) {
|
||||
if (renderedFramesCount.getAndIncrement() < 2) {
|
||||
// Record only the first and replayed frames.
|
||||
readImageBuffers.add(
|
||||
BitmapPixelTestUtil.copyByteBufferFromRbga8888Image(image));
|
||||
} else {
|
||||
readAllOutputFrames.open();
|
||||
}
|
||||
}
|
||||
},
|
||||
Util.createHandlerForCurrentOrMainLooper());
|
||||
|
||||
setOutputSurfaceAndSizeOnPlayer(
|
||||
player,
|
||||
videoRenderer,
|
||||
outputImageReader.getSurface(),
|
||||
new Size(MP4_ASSET.videoFormat.width, MP4_ASSET.videoFormat.height));
|
||||
player.setPlayWhenReady(false);
|
||||
AdjustableContrast contrast = new AdjustableContrast();
|
||||
player.setVideoEffects(ImmutableList.of(createTimestampOverlay(), contrast));
|
||||
|
||||
// Adding an EventLogger to use its log output in case the test fails.
|
||||
player.addAnalyticsListener(new EventLogger());
|
||||
player.addListener(
|
||||
new Player.Listener() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
||||
if (playbackState == STATE_ENDED) {
|
||||
playerEnded.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
player.setVideoFrameMetadataListener(
|
||||
(bufferPresentationTimeUs, releaseTimeNs, format, mediaFormat) -> {
|
||||
// The buffer presentation time is offset with rendererOffset.
|
||||
if (bufferPresentationTimeUs != 1_000_000_000_000L) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstFrameRenderedCount.get() == 0) {
|
||||
// Render the current frame, and redraw a frame with some delay. This is to ensure
|
||||
// that the first frame is rendered with the original effect, and the second
|
||||
// frame is rendered with the new effect. Following this call, the first frame
|
||||
// will be rendered twicw.
|
||||
mainHandler.postDelayed(
|
||||
() -> {
|
||||
contrast.changeContrast(-0.8f);
|
||||
player.setVideoEffects(VideoFrameProcessor.REDRAW);
|
||||
},
|
||||
/* delayMillis= */ 500);
|
||||
} else if (firstFrameRenderedCount.get() == 1) {
|
||||
// Redraw another frame. This renders the first frame for the third time.
|
||||
instrumentation.runOnMainSync(
|
||||
() -> player.setVideoEffects(VideoFrameProcessor.REDRAW));
|
||||
} else {
|
||||
instrumentation.runOnMainSync(player::play);
|
||||
}
|
||||
firstFrameRenderedCount.getAndIncrement();
|
||||
});
|
||||
player.setMediaItem(MediaItem.fromUri(MP4_ASSET.uri));
|
||||
player.prepare();
|
||||
});
|
||||
|
||||
if (!playerEnded.block(TEST_TIMEOUT_MS)) {
|
||||
throw new TimeoutException(
|
||||
Util.formatInvariant("Playback not ended in %d ms.", TEST_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
if (!readAllOutputFrames.block(TEST_TIMEOUT_MS)) {
|
||||
throw new TimeoutException(
|
||||
Util.formatInvariant(
|
||||
"Haven't received all frames in %d ms after playback ends.", TEST_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
ArrayList<Float> averagePixelDifferences =
|
||||
new ArrayList<>(/* initialCapacity= */ readImageBuffers.size());
|
||||
for (int i = 0; i < readImageBuffers.size(); i++) {
|
||||
Bitmap actualBitmap = createArgb8888BitmapFromRgba8888ImageBuffer(readImageBuffers.get(i));
|
||||
float averagePixelAbsoluteDifference =
|
||||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(
|
||||
/* expected= */ readBitmap(
|
||||
Util.formatInvariant("%s/%s/frame_%d.png", TEST_DIRECTORY, testId, i)),
|
||||
/* actual= */ actualBitmap,
|
||||
/* testId= */ Util.formatInvariant("%s_frame_%d", testId, i));
|
||||
averagePixelDifferences.add(averagePixelAbsoluteDifference);
|
||||
}
|
||||
|
||||
for (int i = 0; i < averagePixelDifferences.size(); i++) {
|
||||
float averagePixelDifference = averagePixelDifferences.get(i);
|
||||
assertWithMessage(
|
||||
Util.formatInvariant(
|
||||
"Frame %d with average pixel difference %f. ", i, averagePixelDifference))
|
||||
.that(averagePixelDifference)
|
||||
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||
}
|
||||
// Played once, replayed twice.
|
||||
assertThat(firstFrameRenderedCount.get()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exoplayerEffectsPreview_withTimestampWrapper_ensuresAllFramesRendered()
|
||||
throws Exception {
|
||||
@ -428,6 +573,22 @@ public class EffectPlaybackPixelTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReplayVideoRenderer extends MediaCodecVideoRenderer {
|
||||
|
||||
public ReplayVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
|
||||
super(new Builder(context).setMediaCodecSelector(mediaCodecSelector));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper(
|
||||
Context context, VideoFrameReleaseControl videoFrameReleaseControl) {
|
||||
return new PlaybackVideoGraphWrapper.Builder(context, videoFrameReleaseControl)
|
||||
.setClock(getClock())
|
||||
.setEnableReplayableCache(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoFrameDroppedVideoRenderer extends MediaCodecVideoRenderer {
|
||||
|
||||
public NoFrameDroppedVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
|
||||
@ -446,4 +607,35 @@ public class EffectPlaybackPixelTest {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AdjustableContrast implements RgbMatrix {
|
||||
private float contrast;
|
||||
|
||||
public void changeContrast(float contrast) {
|
||||
this.contrast = contrast;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getMatrix(long presentationTimeUs, boolean useHdr) {
|
||||
float contrastFactor = (1 + contrast) / (1.0001f - contrast);
|
||||
return new float[] {
|
||||
contrastFactor,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
contrastFactor,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
contrastFactor,
|
||||
0.0f,
|
||||
(1.0f - contrastFactor) * 0.5f,
|
||||
(1.0f - contrastFactor) * 0.5f,
|
||||
(1.0f - contrastFactor) * 0.5f,
|
||||
1.0f
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user