Commit b39721f4 by jaewan Committed by Oliver Woodman

Make ExoPlayer as the trusted source of playlist

Playlist can now be obtained directly from Timeline windows
in any state. So make ExoPlayer as the trusted source of
playlist, instead of SessionPlayerConnector.

PlayerWrapper still need to keep the list of media items.
It's used to detect whether the Timeline change is caused by
changes in media items or not, and only notify
SessionPlayer.PlayerCallback#onPlaylistChanged() only when
the playlist is really changed.

PiperOrigin-RevId: 327231820
parent 79a846eb
...@@ -33,6 +33,7 @@ import android.os.Build.VERSION_CODES; ...@@ -33,6 +33,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.media.AudioAttributesCompat; import androidx.media.AudioAttributesCompat;
import androidx.media2.common.MediaItem; import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata; import androidx.media2.common.MediaMetadata;
...@@ -61,6 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -61,6 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
...@@ -798,6 +800,73 @@ public class SessionPlayerConnectorTest { ...@@ -798,6 +800,73 @@ public class SessionPlayerConnectorTest {
@Test @Test
@LargeTest @LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerBeforePrepare_notifiesOnPlaylistChanged()
throws Exception {
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
for (MediaItem mediaItem : playlistToExoPlayer) {
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
}
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor,
new SessionPlayer.PlayerCallback() {
@Override
public void onPlaylistChanged(
@NonNull SessionPlayer player,
@Nullable List<MediaItem> list,
@Nullable MediaMetadata metadata) {
if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
onPlaylistChangedLatch.countDown();
}
}
});
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void setPlaylist_byUnderlyingPlayerAfterPrepare_notifiesOnPlaylistChanged()
throws Exception {
List<MediaItem> playlistToSessionPlayer = TestUtils.createPlaylist(2);
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
for (MediaItem mediaItem : playlistToExoPlayer) {
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
}
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(1);
sessionPlayerConnector.registerPlayerCallback(
executor,
new SessionPlayer.PlayerCallback() {
@Override
public void onPlaylistChanged(
@NonNull SessionPlayer player,
@Nullable List<MediaItem> list,
@Nullable MediaMetadata metadata) {
if (ObjectsCompat.equals(list, playlistToExoPlayer)) {
onPlaylistChangedLatch.countDown();
}
}
});
sessionPlayerConnector.prepare();
sessionPlayerConnector.setPlaylist(playlistToSessionPlayer, /* metadata= */ null);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(() -> playerTestRule.getSimpleExoPlayer().setMediaItems(exoMediaItems));
assertThat(onPlaylistChangedLatch.await(PLAYLIST_CHANGE_WAIT_TIME_MS, MILLISECONDS)).isTrue();
}
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void addPlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception { public void addPlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
List<MediaItem> playlist = TestUtils.createPlaylist(10); List<MediaItem> playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null)); assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
...@@ -862,7 +931,7 @@ public class SessionPlayerConnectorTest { ...@@ -862,7 +931,7 @@ public class SessionPlayerConnectorTest {
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2); CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
int replaceIndex = 2; int replaceIndex = 2;
MediaItem newMediaItem = TestUtils.createMediaItem(); MediaItem newMediaItem = TestUtils.createMediaItem(R.raw.video_big_buck_bunny);
playlist.set(replaceIndex, newMediaItem); playlist.set(replaceIndex, newMediaItem);
sessionPlayerConnector.registerPlayerCallback( sessionPlayerConnector.registerPlayerCallback(
executor, executor,
...@@ -1185,6 +1254,32 @@ public class SessionPlayerConnectorTest { ...@@ -1185,6 +1254,32 @@ public class SessionPlayerConnectorTest {
assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PLAYING); assertThat(sessionPlayerConnector.getPlayerState()).isEqualTo(PLAYER_STATE_PLAYING);
} }
@Test
@LargeTest
public void getPlaylist_returnsPlaylistInUnderlyingPlayer() {
List<MediaItem> playlistToExoPlayer = TestUtils.createPlaylist(4);
DefaultMediaItemConverter converter = new DefaultMediaItemConverter();
List<com.google.android.exoplayer2.MediaItem> exoMediaItems = new ArrayList<>();
for (MediaItem mediaItem : playlistToExoPlayer) {
exoMediaItems.add(converter.convertToExoPlayerMediaItem(mediaItem));
}
AtomicReference<List<MediaItem>> playlistFromSessionPlayer = new AtomicReference<>();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer();
simpleExoPlayer.setMediaItems(exoMediaItems);
try (SessionPlayerConnector sessionPlayer =
new SessionPlayerConnector(simpleExoPlayer, converter)) {
List<MediaItem> playlist = sessionPlayer.getPlaylist();
playlistFromSessionPlayer.set(playlist);
}
});
assertThat(playlistFromSessionPlayer.get()).isEqualTo(playlistToExoPlayer);
}
private class PlayerCallbackForPlaylist extends SessionPlayer.PlayerCallback { private class PlayerCallbackForPlaylist extends SessionPlayer.PlayerCallback {
private List<MediaItem> playlist; private List<MediaItem> playlist;
private CountDownLatch onCurrentMediaItemChangedLatch; private CountDownLatch onCurrentMediaItemChangedLatch;
......
...@@ -37,10 +37,10 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { ...@@ -37,10 +37,10 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
throw new IllegalStateException("CallbackMediaItem isn't supported"); throw new IllegalStateException("CallbackMediaItem isn't supported");
} }
MediaItem.Builder exoplayerMediaItemBuilder = new MediaItem.Builder(); MediaItem.Builder exoPlayerMediaItemBuilder = new MediaItem.Builder();
// Set mediaItem as tag for creating MediaSource via MediaSourceFactory methods. // Set mediaItem as tag for creating MediaSource via MediaSourceFactory methods.
exoplayerMediaItemBuilder.setTag(androidXMediaItem); exoPlayerMediaItemBuilder.setTag(androidXMediaItem);
// Media ID or URI must be present. Get it from androidx.MediaItem if possible. // Media ID or URI must be present. Get it from androidx.MediaItem if possible.
@Nullable Uri uri = null; @Nullable Uri uri = null;
...@@ -61,31 +61,31 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { ...@@ -61,31 +61,31 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
// Generate a Uri to make it non-null. If not, tag will be ignored. // Generate a Uri to make it non-null. If not, tag will be ignored.
uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode()); uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode());
} }
exoplayerMediaItemBuilder.setUri(uri); exoPlayerMediaItemBuilder.setUri(uri);
exoplayerMediaItemBuilder.setMediaId(mediaId); exoPlayerMediaItemBuilder.setMediaId(mediaId);
if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
exoplayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition()); exoPlayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition());
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true); exoPlayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
} }
if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
exoplayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition()); exoPlayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition());
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true); exoPlayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
} }
return exoplayerMediaItemBuilder.build(); return exoPlayerMediaItemBuilder.build();
} }
@Override @Override
public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem) { public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoPlayerMediaItem) {
Assertions.checkNotNull(exoplayerMediaItem); Assertions.checkNotNull(exoPlayerMediaItem);
MediaItem.PlaybackProperties playbackProperties = MediaItem.PlaybackProperties playbackProperties =
Assertions.checkNotNull(exoplayerMediaItem.playbackProperties); Assertions.checkNotNull(exoPlayerMediaItem.playbackProperties);
@Nullable Object tag = playbackProperties.tag; @Nullable Object tag = playbackProperties.tag;
if (!(tag instanceof androidx.media2.common.MediaItem)) { if (tag instanceof androidx.media2.common.MediaItem) {
throw new IllegalStateException( return (androidx.media2.common.MediaItem) tag;
"MediaItem tag must be an instance of androidx.media2.common.MediaItem");
} }
return (androidx.media2.common.MediaItem) tag;
return new UriMediaItem.Builder(playbackProperties.uri).build();
} }
} }
...@@ -33,5 +33,5 @@ public interface MediaItemConverter { ...@@ -33,5 +33,5 @@ public interface MediaItemConverter {
* Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.MediaItem * Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.MediaItem
* AndroidX MediaItem}. * AndroidX MediaItem}.
*/ */
androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem); androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoPlayerMediaItem);
} }
...@@ -41,7 +41,6 @@ import java.util.Map; ...@@ -41,7 +41,6 @@ import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
...@@ -95,7 +94,6 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -95,7 +94,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
// Should be only accessed on the executor, which is currently single-threaded. // Should be only accessed on the executor, which is currently single-threaded.
@Nullable private MediaItem currentMediaItem; @Nullable private MediaItem currentMediaItem;
@Nullable private List<MediaItem> currentPlaylist;
/** /**
* Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands. * Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands.
...@@ -124,19 +122,8 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -124,19 +122,8 @@ public final class SessionPlayerConnector extends SessionPlayer {
taskHandler = new PlayerHandler(player.getApplicationLooper()); taskHandler = new PlayerHandler(player.getApplicationLooper());
taskHandlerExecutor = taskHandler::postOrRun; taskHandlerExecutor = taskHandler::postOrRun;
ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener(); ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener();
PlayerWrapper playerWrapper = this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
this.player = playerWrapper;
playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler); playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
@SuppressWarnings("assignment.type.incompatible")
@Initialized
SessionPlayerConnector initializedThis = this;
initializedThis.<Void>runPlayerCallableBlocking(
/* callable= */ () -> {
playerWrapper.reset();
return null;
});
} }
@Override @Override
...@@ -258,7 +245,6 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -258,7 +245,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
ListenableFuture<PlayerResult> result = ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand( playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item)); PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result; return result;
} }
...@@ -281,7 +267,6 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -281,7 +267,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand( playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST, PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST,
/* command= */ () -> player.setPlaylist(playlist, metadata)); /* command= */ () -> player.setPlaylist(playlist, metadata));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result; return result;
} }
...@@ -294,7 +279,6 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -294,7 +279,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand( playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM, PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
/* command= */ () -> player.addPlaylistItem(index, item)); /* command= */ () -> player.addPlaylistItem(index, item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result; return result;
} }
...@@ -305,7 +289,6 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -305,7 +289,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand( playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM, PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
/* command= */ () -> player.removePlaylistItem(index)); /* command= */ () -> player.removePlaylistItem(index));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result; return result;
} }
...@@ -318,7 +301,6 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -318,7 +301,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand( playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM, PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
/* command= */ () -> player.replacePlaylistItem(index, item)); /* command= */ () -> player.replacePlaylistItem(index, item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result; return result;
} }
...@@ -385,7 +367,7 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -385,7 +367,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
@Override @Override
@Nullable @Nullable
public List<MediaItem> getPlaylist() { public List<MediaItem> getPlaylist() {
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getCachedPlaylist); return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist);
} }
@Override @Override
...@@ -558,25 +540,18 @@ public final class SessionPlayerConnector extends SessionPlayer { ...@@ -558,25 +540,18 @@ public final class SessionPlayerConnector extends SessionPlayer {
} }
private void handlePlaylistChangedOnHandler() { private void handlePlaylistChangedOnHandler() {
List<MediaItem> currentPlaylist = player.getCachedPlaylist(); List<MediaItem> currentPlaylist = player.getPlaylist();
boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist);
this.currentPlaylist = currentPlaylist;
MediaMetadata playlistMetadata = player.getPlaylistMetadata(); MediaMetadata playlistMetadata = player.getPlaylistMetadata();
MediaItem currentMediaItem = player.getCurrentMediaItem(); MediaItem currentMediaItem = player.getCurrentMediaItem();
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem); boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
this.currentMediaItem = currentMediaItem; this.currentMediaItem = currentMediaItem;
if (!notifyCurrentMediaItem && !notifyCurrentPlaylist) {
return;
}
long currentPosition = getCurrentPosition(); long currentPosition = getCurrentPosition();
notifySessionPlayerCallback( notifySessionPlayerCallback(
callback -> { callback -> {
if (notifyCurrentPlaylist) { callback.onPlaylistChanged(
callback.onPlaylistChanged( SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
}
if (notifyCurrentMediaItem) { if (notifyCurrentMediaItem) {
Assertions.checkNotNull( Assertions.checkNotNull(
currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null"); currentMediaItem, "PlaylistManager#currentMediaItem() cannot be changed to null");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment