diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index 172c8e6007..834a4b29ad 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -745,7 +745,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private void setSeekMap(SeekMap seekMap) { this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET); - if (seekMap.getDurationUs() == C.TIME_UNSET && durationUs == C.TIME_UNSET) { + if (seekMap.getDurationUs() == C.TIME_UNSET && durationUs != C.TIME_UNSET) { this.seekMap = new ForwardingSeekMap(this.seekMap) { @Override diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ImagePlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ImagePlaybackTest.java new file mode 100644 index 0000000000..c7f0ea9f6f --- /dev/null +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/ImagePlaybackTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 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.exoplayer.e2etest; + +import static com.google.common.truth.Truth.assertThat; +import static org.robolectric.annotation.GraphicsMode.Mode.LEGACY; + +import android.content.Context; +import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; +import androidx.media3.common.util.Clock; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.test.utils.CapturingRenderersFactory; +import androidx.media3.test.utils.DumpFileAsserts; +import androidx.media3.test.utils.FakeClock; +import androidx.media3.test.utils.robolectric.PlaybackOutput; +import androidx.media3.test.utils.robolectric.TestPlayerRunHelper; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.ParameterizedRobolectricTestRunner; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameter; +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; +import org.robolectric.annotation.GraphicsMode; + +/** End-to-end tests using image samples. */ +@RunWith(ParameterizedRobolectricTestRunner.class) +@GraphicsMode(value = LEGACY) +public class ImagePlaybackTest { + + @Parameter public String inputFile; + + @Parameters(name = "{0}") + public static ImmutableList mediaSamples() { + // TODO(b/289989736): When extraction for other types of images is implemented, add those image + // types to this list. + return ImmutableList.of("png/non-motion-photo-shortened.png"); + } + + @Test + public void test() throws Exception { + Context applicationContext = ApplicationProvider.getApplicationContext(); + CapturingRenderersFactory renderersFactory = + new CapturingRenderersFactory(applicationContext, /* addImageRenderer= */ true); + Clock clock = new FakeClock(/* isAutoAdvancing= */ true); + ExoPlayer player = + new ExoPlayer.Builder(applicationContext, renderersFactory).setClock(clock).build(); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory); + long durationMs = 5 * C.MILLIS_PER_SECOND; + player.setMediaItem( + new MediaItem.Builder() + .setUri("asset:///media/" + inputFile) + .setImageDurationMs(durationMs) + .build()); + player.prepare(); + + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + long playerStartedMs = clock.elapsedRealtime(); + player.play(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs; + player.release(); + + assertThat(playbackDurationMs).isEqualTo(durationMs); + DumpFileAsserts.assertOutput( + applicationContext, playbackOutput, "playbackdumps/" + inputFile + ".dump"); + } +} diff --git a/libraries/test_data/src/test/assets/playbackdumps/png/non-motion-photo-shortened.png.dump b/libraries/test_data/src/test/assets/playbackdumps/png/non-motion-photo-shortened.png.dump new file mode 100644 index 0000000000..595fea03ad --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/png/non-motion-photo-shortened.png.dump @@ -0,0 +1,6 @@ +AudioSink: + buffer count = 0 +ImageOutput: + rendered image count = 1 + image output #1: + presentationTimeUs = 0 diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingImageOutput.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingImageOutput.java new file mode 100644 index 0000000000..d240d424a0 --- /dev/null +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingImageOutput.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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.test.utils; + +import android.graphics.Bitmap; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.image.ImageOutput; +import androidx.media3.test.utils.Dumper.Dumpable; +import java.util.ArrayList; +import java.util.List; + +/** A {@link ImageOutput} that captures image availability events. */ +@UnstableApi +public final class CapturingImageOutput implements Dumpable, ImageOutput { + + private final List renderedBitmaps; + + private int imageCount; + + public CapturingImageOutput() { + renderedBitmaps = new ArrayList<>(); + } + + @Override + public void onImageAvailable(long presentationTimeUs, Bitmap bitmap) { + imageCount++; + renderedBitmaps.add( + dumper -> { + dumper.startBlock("image output #" + imageCount); + dumper.add("presentationTimeUs", presentationTimeUs); + dumper.endBlock(); + }); + } + + @Override + public void dump(Dumper dumper) { + dumper.add("rendered image count", imageCount); + for (Dumpable dumpable : renderedBitmaps) { + dumpable.dump(dumper); + } + } +} diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java index 5a22c0b59c..6814b7cb6d 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CapturingRenderersFactory.java @@ -36,6 +36,9 @@ import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.audio.DefaultAudioSink; import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer; +import androidx.media3.exoplayer.image.ImageDecoder; +import androidx.media3.exoplayer.image.ImageOutput; +import androidx.media3.exoplayer.image.ImageRenderer; import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; import androidx.media3.exoplayer.metadata.MetadataOutput; @@ -57,7 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link RenderersFactory} that captures interactions with the audio and video {@link - * MediaCodecAdapter} instances. + * MediaCodecAdapter} instances and {@link ImageOutput} instances. * *

The captured interactions can be used in a test assertion via the {@link Dumper.Dumpable} * interface. @@ -66,13 +69,33 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpable { private final Context context; + private final boolean addImageRenderer; private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory; private final CapturingAudioSink audioSink; + private final CapturingImageOutput imageOutput; + /** + * Creates an instance. + * + *

The factory will not include an {@link ImageRenderer}. + */ public CapturingRenderersFactory(Context context) { + this(context, /* addImageRenderer= */ false); + } + + /** + * Creates an instance. + * + * @param context The {@link Context}. + * @param addImageRenderer Whether to add the image renderer to the list of renderers created in + * {@link #createRenderers}. + */ + public CapturingRenderersFactory(Context context, boolean addImageRenderer) { this.context = context; this.mediaCodecAdapterFactory = new CapturingMediaCodecAdapter.Factory(); this.audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder(context).build()); + this.imageOutput = new CapturingImageOutput(); + this.addImageRenderer = addImageRenderer; } @Override @@ -82,47 +105,53 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, MetadataOutput metadataRendererOutput) { - return new Renderer[] { - new MediaCodecVideoRenderer( - context, - mediaCodecAdapterFactory, - MediaCodecSelector.DEFAULT, - DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS, - /* enableDecoderFallback= */ false, - eventHandler, - videoRendererEventListener, - DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY) { - @Override - protected boolean shouldDropOutputBuffer( - long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { - // Do not drop output buffers due to slow processing. - return false; - } + ArrayList temp = new ArrayList<>(); + temp.add( + new MediaCodecVideoRenderer( + context, + mediaCodecAdapterFactory, + MediaCodecSelector.DEFAULT, + DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS, + /* enableDecoderFallback= */ false, + eventHandler, + videoRendererEventListener, + DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY) { + @Override + protected boolean shouldDropOutputBuffer( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + // Do not drop output buffers due to slow processing. + return false; + } - @Override - protected boolean shouldDropBuffersToKeyframe( - long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { - // Do not drop output buffers due to slow processing. - return false; - } + @Override + protected boolean shouldDropBuffersToKeyframe( + long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) { + // Do not drop output buffers due to slow processing. + return false; + } - @Override - protected boolean shouldSkipBuffersWithIdenticalReleaseTime() { - // Do not skip buffers with identical vsync times as we can't control this from tests. - return false; - } - }, - new MediaCodecAudioRenderer( - context, - mediaCodecAdapterFactory, - MediaCodecSelector.DEFAULT, - /* enableDecoderFallback= */ false, - eventHandler, - audioRendererEventListener, - audioSink), - new TextRenderer(textRendererOutput, eventHandler.getLooper()), - new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper()) - }; + @Override + protected boolean shouldSkipBuffersWithIdenticalReleaseTime() { + // Do not skip buffers with identical vsync times as we can't control this from tests. + return false; + } + }); + temp.add( + new MediaCodecAudioRenderer( + context, + mediaCodecAdapterFactory, + MediaCodecSelector.DEFAULT, + /* enableDecoderFallback= */ false, + eventHandler, + audioRendererEventListener, + audioSink)); + temp.add(new TextRenderer(textRendererOutput, eventHandler.getLooper())); + temp.add(new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper())); + + if (addImageRenderer) { + temp.add(new ImageRenderer(ImageDecoder.Factory.DEFAULT, imageOutput)); + } + return temp.toArray(new Renderer[] {}); } @Override @@ -131,6 +160,11 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa dumper.startBlock("AudioSink"); audioSink.dump(dumper); dumper.endBlock(); + if (addImageRenderer) { + dumper.startBlock("ImageOutput"); + imageOutput.dump(dumper); + dumper.endBlock(); + } } /**