diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index f3035b0683..4c0c840b4c 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -2265,10 +2265,20 @@ import java.util.concurrent.atomic.AtomicBoolean; Player.DISCONTINUITY_REASON_AUTO_TRANSITION); resetPendingPauseAtEndOfPeriod(); updatePlaybackPositions(); + allowRenderersToRenderStartOfStreams(); advancedPlayingPeriod = true; } } + private void allowRenderersToRenderStartOfStreams() { + TrackSelectorResult playingTracks = queue.getPlayingPeriod().getTrackSelectorResult(); + for (int i = 0; i < renderers.length; i++) { + if (playingTracks.isRendererEnabled(i)) { + renderers[i].enableMayRenderStartOfStream(); + } + } + } + private void resetPendingPauseAtEndOfPeriod() { @Nullable MediaPeriodHolder playingPeriod = queue.getPlayingPeriod(); pendingPauseAtEndOfPeriod = diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java index ff994b1f8a..17311ca7d1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -462,6 +462,15 @@ public interface Renderer extends PlayerMessage.Target { default void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed) throws ExoPlaybackException {} + /** + * Enables this renderer to render the start of the stream even if the state is not {@link + * #STATE_STARTED} yet. + * + *
This is used to update the value of {@code mayRenderStartOfStream} passed to {@link
+ * #enable}.
+ */
+ default void enableMayRenderStartOfStream() {}
+
/**
* Incrementally renders the {@link SampleStream}.
*
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DecoderVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DecoderVideoRenderer.java
index 91a90f4502..f263d19d20 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DecoderVideoRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DecoderVideoRenderer.java
@@ -280,6 +280,11 @@ public abstract class DecoderVideoRenderer extends BaseRenderer {
renderedFirstFrameAfterEnable = false;
}
+ @Override
+ public void enableMayRenderStartOfStream() {
+ mayRenderFirstFrameAfterEnableIfNotStarted = true;
+ }
+
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
inputStreamEnded = false;
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
index d65e4da7cf..9bf9a88cb9 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
@@ -605,6 +605,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
renderedFirstFrameAfterEnable = false;
}
+ @Override
+ public void enableMayRenderStartOfStream() {
+ mayRenderFirstFrameAfterEnableIfNotStarted = true;
+ }
+
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
super.onPositionReset(positionUs, joining);
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
index 3b11df3ca0..bbd205b523 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
@@ -62,6 +62,7 @@ import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUnt
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
+import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlayWhenReady;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
@@ -129,6 +130,7 @@ import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
+import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
@@ -178,6 +180,7 @@ import androidx.media3.test.utils.FakeTrackSelection;
import androidx.media3.test.utils.FakeTrackSelector;
import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.media3.test.utils.TestExoPlayerBuilder;
+import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -201,6 +204,7 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -227,9 +231,15 @@ public final class ExoPlayerTest {
*/
private static final int TIMEOUT_MS = 10_000;
+ private static final String SAMPLE_URI = "asset://android_asset/media/mp4/sample.mp4";
+
private Context context;
private Timeline placeholderTimeline;
+ @Rule
+ public ShadowMediaCodecConfig mediaCodecConfig =
+ ShadowMediaCodecConfig.forAllSupportedMimeTypes();
+
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
@@ -12542,7 +12552,7 @@ public final class ExoPlayerTest {
MediaItem mediaItem =
new MediaItem.Builder()
.setMediaId("id")
- .setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
+ .setUri(Uri.parse(SAMPLE_URI))
.setMediaMetadata(mediaMetadata)
.build();
player.setMediaItem(mediaItem);
@@ -13729,6 +13739,110 @@ public final class ExoPlayerTest {
player.release();
}
+ @Test
+ public void pauseAtEndOfMediaItem_withSecondStreamDelayed_playsSuccessfully() throws Exception {
+ // Set allowed video joining time to zero so that the renderer is not automatically considered
+ // ready when we re-enable it at the transition.
+ ExoPlayer player =
+ new ExoPlayer.Builder(context)
+ .setRenderersFactory(
+ new DefaultRenderersFactory(context).setAllowedVideoJoiningTimeMs(0))
+ .setClock(new FakeClock(/* isAutoAdvancing= */ true))
+ .build();
+ player.setPauseAtEndOfMediaItems(true);
+ Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
+ player.setVideoSurface(surface);
+ player.addMediaItem(MediaItem.fromUri(SAMPLE_URI));
+ Timeline timeline = new FakeTimeline();
+ AtomicBoolean allowStreamRead = new AtomicBoolean();
+ MediaSource delayedStreamSource =
+ new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
+ @Override
+ protected MediaPeriod createMediaPeriod(
+ MediaPeriodId id,
+ TrackGroupArray trackGroupArray,
+ Allocator allocator,
+ MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
+ DrmSessionManager drmSessionManager,
+ DrmSessionEventListener.EventDispatcher drmEventDispatcher,
+ @Nullable TransferListener transferListener) {
+ long startPositionUs =
+ -timeline
+ .getPeriodByUid(id.periodUid, new Timeline.Period())
+ .getPositionInWindowUs();
+ // Add enough samples to the source so that the decoder can't decode everything at once.
+ return new FakeMediaPeriod(
+ trackGroupArray,
+ allocator,
+ (format, mediaPerioid) ->
+ ImmutableList.of(
+ oneByteSample(startPositionUs, C.BUFFER_FLAG_KEY_FRAME),
+ oneByteSample(startPositionUs + 10_000),
+ oneByteSample(startPositionUs + 20_000),
+ oneByteSample(startPositionUs + 30_000),
+ oneByteSample(startPositionUs + 40_000),
+ oneByteSample(startPositionUs + 50_000),
+ oneByteSample(startPositionUs + 60_000),
+ oneByteSample(startPositionUs + 70_000),
+ oneByteSample(startPositionUs + 80_000),
+ oneByteSample(startPositionUs + 90_000),
+ oneByteSample(startPositionUs + 100_000),
+ END_OF_STREAM_ITEM),
+ mediaSourceEventDispatcher,
+ drmSessionManager,
+ drmEventDispatcher,
+ /* deferOnPrepared= */ false) {
+ @Override
+ protected FakeSampleStream createSampleStream(
+ Allocator allocator,
+ @Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
+ DrmSessionManager drmSessionManager,
+ DrmSessionEventListener.EventDispatcher drmEventDispatcher,
+ Format initialFormat,
+ List