diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 0d6f24d98e..c32260fc23 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -2888,6 +2888,15 @@ public abstract class SimpleBasePlayer extends BasePlayer { // Assign new state immediately such that all getters return the right values, but use a // snapshot of the previous and new state so that listener invocations are triggered correctly. this.state = newState; + if (newState.hasPositionDiscontinuity || newState.newlyRenderedFirstFrame) { + // Clear one-time events to avoid signalling them again later. + this.state = + this.state + .buildUpon() + .clearPositionDiscontinuity() + .setNewlyRenderedFirstFrame(false) + .build(); + } boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady; boolean playbackStateChanged = previousState.playbackState != newState.playbackState; @@ -2914,7 +2923,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { PositionInfo positionInfo = getPositionInfo( newState, - /* useDiscontinuityPosition= */ state.hasPositionDiscontinuity, + /* useDiscontinuityPosition= */ newState.hasPositionDiscontinuity, window, period); listeners.queueEvent( @@ -2928,9 +2937,9 @@ public abstract class SimpleBasePlayer extends BasePlayer { if (mediaItemTransitionReason != C.INDEX_UNSET) { @Nullable MediaItem mediaItem = - state.timeline.isEmpty() + newState.timeline.isEmpty() ? null - : state.playlist.get(state.currentMediaItemIndex).mediaItem; + : newState.playlist.get(state.currentMediaItemIndex).mediaItem; listeners.queueEvent( Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason)); diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 498fb60068..b6b65b62c8 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -1697,6 +1697,82 @@ public class SimpleBasePlayerTest { verify(listener, never()).onMediaItemTransition(any(), anyInt()); } + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void invalidateStateAndOtherOperation_withDiscontinuity_reportsDiscontinuityOnlyOnce() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0).build())) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_INTERNAL, /* discontinuityPositionMs= */ 2000) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handlePrepare() { + // We just care about the placeholder state, so return an unfulfilled future. + return SettableFuture.create(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.invalidateState(); + player.prepare(); + + // Assert listener calls (in particular getting only a single discontinuity). + verify(listener) + .onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_INTERNAL)); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); + verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING); + verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_BUFFERING); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener call. + @Test + public void + invalidateStateAndOtherOperation_withRenderedFirstFrame_reportsRenderedFirstFrameOnlyOnce() { + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 0).build())) + .setNewlyRenderedFirstFrame(true) + .build(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handlePrepare() { + // We just care about the placeholder state, so return an unfulfilled future. + return SettableFuture.create(); + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.invalidateState(); + player.prepare(); + + // Assert listener calls (in particular getting only a single rendered first frame). + verify(listener).onRenderedFirstFrame(); + verify(listener).onPlaybackStateChanged(Player.STATE_BUFFERING); + verify(listener).onPlayerStateChanged(/* playWhenReady= */ false, Player.STATE_BUFFERING); + verifyNoMoreInteractions(listener); + } + @Test public void invalidateState_duringAsyncMethodHandling_isIgnored() { State state1 =