Update session position info on timeline change

This fixes an inconsistent state of the `PlayerInfo` when the index of the playing
media item is changed by a playlist modification. In this inconsistent state,
calling `Playerinfo.getCurrentMediaItem` can produce an
`ArrayIndexOutOfBoundException` (see stack trace in GH issue).

This change takes the following measurements:

- always update sessionPosition and timeline of the PlayerInfo together in
  `MediaSessionImpl.PlayerListener` where the PlayerInfo originates from
- add an assertion to avoid building a `PlayerInfo` instance in an inconsistent
  state
- reduce the window of opportunity for concurrent access to
  `mediaSessionImpl.playerInfo` when dispatching player info changes in
  `MediaSessionImpl`

Issue: androidx/media#51
PiperOrigin-RevId: 444812661
This commit is contained in:
bachinger 2022-04-27 11:48:01 +01:00 committed by Ian Baker
parent 9369348d6f
commit dee83cc7db
4 changed files with 39 additions and 17 deletions

View File

@ -80,6 +80,9 @@
* Session: * Session:
* Fix NPE in MediaControllerImplLegacy * Fix NPE in MediaControllerImplLegacy
([#59](https://github.com/androidx/media/pull/59)) ([#59](https://github.com/androidx/media/pull/59))
* Update session position info on timeline
change([#51](https://github.com/androidx/media/issues/51))
* Data sources: * Data sources:
* Rename `DummyDataSource` to `PlaceHolderDataSource`. * Rename `DummyDataSource` to `PlaceHolderDataSource`.
* Remove deprecated symbols: * Remove deprecated symbols:

View File

@ -753,7 +753,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int newCurrentMediaItemIndex = int newCurrentMediaItemIndex =
calculateCurrentItemIndexAfterAddItems(currentMediaItemIndex, index, mediaItems.size()); calculateCurrentItemIndexAfterAddItems(currentMediaItemIndex, index, mediaItems.size());
PlayerInfo maskedPlayerInfo = PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimeline(newQueueTimeline, newCurrentMediaItemIndex); controllerInfo.playerInfo.copyWithTimelineAndMediaItemIndex(
newQueueTimeline, newCurrentMediaItemIndex);
ControllerInfo maskedControllerInfo = ControllerInfo maskedControllerInfo =
new ControllerInfo( new ControllerInfo(
maskedPlayerInfo, maskedPlayerInfo,
@ -801,7 +802,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
+ " new current item"); + " new current item");
} }
PlayerInfo maskedPlayerInfo = PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimeline(newQueueTimeline, newCurrentMediaItemIndex); controllerInfo.playerInfo.copyWithTimelineAndMediaItemIndex(
newQueueTimeline, newCurrentMediaItemIndex);
ControllerInfo maskedControllerInfo = ControllerInfo maskedControllerInfo =
new ControllerInfo( new ControllerInfo(
@ -861,7 +863,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
QueueTimeline newQueueTimeline = QueueTimeline newQueueTimeline =
queueTimeline.copyWithMovedMediaItems(fromIndex, toIndex, newIndex); queueTimeline.copyWithMovedMediaItems(fromIndex, toIndex, newIndex);
PlayerInfo maskedPlayerInfo = PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimeline(newQueueTimeline, newCurrentMediaItemIndex); controllerInfo.playerInfo.copyWithTimelineAndMediaItemIndex(
newQueueTimeline, newCurrentMediaItemIndex);
ControllerInfo maskedControllerInfo = ControllerInfo maskedControllerInfo =
new ControllerInfo( new ControllerInfo(

View File

@ -366,7 +366,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
(controller, seq) -> controller.sendCustomCommand(seq, command, args)); (controller, seq) -> controller.sendCustomCommand(seq, command, args));
} }
private void dispatchOnPlayerInfoChanged(boolean excludeTimeline) { private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeTimeline) {
List<ControllerInfo> controllers = List<ControllerInfo> controllers =
sessionStub.getConnectedControllersManager().getConnectedControllers(); sessionStub.getConnectedControllersManager().getConnectedControllers();
@ -910,7 +910,9 @@ import org.checkerframework.checker.initialization.qual.Initialized;
if (player == null) { if (player == null) {
return; return;
} }
session.playerInfo = session.playerInfo.copyWithTimeline(timeline); session.playerInfo =
session.playerInfo.copyWithTimelineAndSessionPositionInfo(
timeline, player.createSessionPositionInfoForBundling());
session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false); session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false);
session.dispatchRemoteControllerTaskToLegacyStub( session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onTimelineChanged(seq, timeline, reason)); (callback, seq) -> callback.onTimelineChanged(seq, timeline, reason));
@ -1177,9 +1179,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
if (msg.what == MSG_PLAYER_INFO_CHANGED) { if (msg.what == MSG_PLAYER_INFO_CHANGED) {
playerInfo = playerInfo =
playerInfo.copyWithSessionPositionInfo( playerInfo.copyWithTimelineAndSessionPositionInfo(
getPlayerWrapper().getCurrentTimeline(),
getPlayerWrapper().createSessionPositionInfoForBundling()); getPlayerWrapper().createSessionPositionInfoForBundling());
dispatchOnPlayerInfoChanged(excludeTimeline); dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline);
excludeTimeline = true; excludeTimeline = true;
} else { } else {
throw new IllegalStateException("Invalid message what=" + msg.what); throw new IllegalStateException("Invalid message what=" + msg.what);

View File

@ -44,6 +44,7 @@ import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue; import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.BundleableUtil;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -271,6 +272,9 @@ import java.util.List;
} }
public PlayerInfo build() { public PlayerInfo build() {
Assertions.checkState(
timeline.isEmpty()
|| sessionPositionInfo.positionInfo.mediaItemIndex < timeline.getWindowCount());
return new PlayerInfo( return new PlayerInfo(
playerError, playerError,
mediaItemTransitionReason, mediaItemTransitionReason,
@ -344,7 +348,7 @@ import java.util.List;
MediaMetadata.EMPTY, MediaMetadata.EMPTY,
/* seekBackIncrementMs= */ 0, /* seekBackIncrementMs= */ 0,
/* seekForwardIncrementMs= */ 0, /* seekForwardIncrementMs= */ 0,
/* maxSeekToPreviousPosition= */ 0, /* maxSeekToPreviousPositionMs= */ 0,
TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT); TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT);
@Nullable public final PlaybackException playerError; @Nullable public final PlaybackException playerError;
@ -429,11 +433,6 @@ import java.util.List;
return new Builder(this).setPlayerError(playerError).build(); return new Builder(this).setPlayerError(playerError).build();
} }
@CheckResult
public PlayerInfo copyWithSessionPositionInfo(SessionPositionInfo sessionPositionInfo) {
return new Builder(this).setSessionPositionInfo(sessionPositionInfo).build();
}
@CheckResult @CheckResult
public PlayerInfo copyWithPlaybackState( public PlayerInfo copyWithPlaybackState(
@Player.State int playbackState, @Nullable PlaybackException playerError) { @Player.State int playbackState, @Nullable PlaybackException playerError) {
@ -454,6 +453,11 @@ import java.util.List;
return new Builder(this).setIsLoading(isLoading).build(); return new Builder(this).setIsLoading(isLoading).build();
} }
@CheckResult
public PlayerInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) {
return new Builder(this).setPlaybackParameters(playbackParameters).build();
}
@CheckResult @CheckResult
public PlayerInfo copyWithPositionInfos( public PlayerInfo copyWithPositionInfos(
PositionInfo oldPositionInfo, PositionInfo oldPositionInfo,
@ -467,8 +471,8 @@ import java.util.List;
} }
@CheckResult @CheckResult
public PlayerInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) { public PlayerInfo copyWithSessionPositionInfo(SessionPositionInfo sessionPositionInfo) {
return new Builder(this).setPlaybackParameters(playbackParameters).build(); return new Builder(this).setSessionPositionInfo(sessionPositionInfo).build();
} }
@CheckResult @CheckResult
@ -477,14 +481,23 @@ import java.util.List;
} }
@CheckResult @CheckResult
public PlayerInfo copyWithTimeline(Timeline timeline, int windowIndex) { public PlayerInfo copyWithTimelineAndSessionPositionInfo(
Timeline timeline, SessionPositionInfo sessionPositionInfo) {
return new Builder(this)
.setTimeline(timeline)
.setSessionPositionInfo(sessionPositionInfo)
.build();
}
@CheckResult
public PlayerInfo copyWithTimelineAndMediaItemIndex(Timeline timeline, int mediaItemIndex) {
return new Builder(this) return new Builder(this)
.setTimeline(timeline) .setTimeline(timeline)
.setSessionPositionInfo( .setSessionPositionInfo(
new SessionPositionInfo( new SessionPositionInfo(
new PositionInfo( new PositionInfo(
sessionPositionInfo.positionInfo.windowUid, sessionPositionInfo.positionInfo.windowUid,
windowIndex, mediaItemIndex,
sessionPositionInfo.positionInfo.mediaItem, sessionPositionInfo.positionInfo.mediaItem,
sessionPositionInfo.positionInfo.periodUid, sessionPositionInfo.positionInfo.periodUid,
sessionPositionInfo.positionInfo.periodIndex, sessionPositionInfo.positionInfo.periodIndex,