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;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.media.AudioAttributesCompat;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
......@@ -61,6 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
......@@ -798,6 +800,73 @@ public class SessionPlayerConnectorTest {
@Test
@LargeTest
@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 {
List<MediaItem> playlist = TestUtils.createPlaylist(10);
assertPlayerResultSuccess(sessionPlayerConnector.setPlaylist(playlist, /* metadata= */ null));
......@@ -862,7 +931,7 @@ public class SessionPlayerConnectorTest {
CountDownLatch onPlaylistChangedLatch = new CountDownLatch(2);
int replaceIndex = 2;
MediaItem newMediaItem = TestUtils.createMediaItem();
MediaItem newMediaItem = TestUtils.createMediaItem(R.raw.video_big_buck_bunny);
playlist.set(replaceIndex, newMediaItem);
sessionPlayerConnector.registerPlayerCallback(
executor,
......@@ -1185,6 +1254,32 @@ public class SessionPlayerConnectorTest {
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 List<MediaItem> playlist;
private CountDownLatch onCurrentMediaItemChangedLatch;
......
......@@ -37,10 +37,10 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
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.
exoplayerMediaItemBuilder.setTag(androidXMediaItem);
exoPlayerMediaItemBuilder.setTag(androidXMediaItem);
// Media ID or URI must be present. Get it from androidx.MediaItem if possible.
@Nullable Uri uri = null;
......@@ -61,31 +61,31 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
// Generate a Uri to make it non-null. If not, tag will be ignored.
uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode());
}
exoplayerMediaItemBuilder.setUri(uri);
exoplayerMediaItemBuilder.setMediaId(mediaId);
exoPlayerMediaItemBuilder.setUri(uri);
exoPlayerMediaItemBuilder.setMediaId(mediaId);
if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
exoplayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition());
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
exoPlayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition());
exoPlayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
}
if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) {
exoplayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition());
exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
exoPlayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition());
exoPlayerMediaItemBuilder.setClipRelativeToDefaultPosition(true);
}
return exoplayerMediaItemBuilder.build();
return exoPlayerMediaItemBuilder.build();
}
@Override
public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem) {
Assertions.checkNotNull(exoplayerMediaItem);
public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoPlayerMediaItem) {
Assertions.checkNotNull(exoPlayerMediaItem);
MediaItem.PlaybackProperties playbackProperties =
Assertions.checkNotNull(exoplayerMediaItem.playbackProperties);
Assertions.checkNotNull(exoPlayerMediaItem.playbackProperties);
@Nullable Object tag = playbackProperties.tag;
if (!(tag instanceof androidx.media2.common.MediaItem)) {
throw new IllegalStateException(
"MediaItem tag must be an instance of androidx.media2.common.MediaItem");
if (tag instanceof androidx.media2.common.MediaItem) {
return (androidx.media2.common.MediaItem) tag;
}
return (androidx.media2.common.MediaItem) tag;
return new UriMediaItem.Builder(playbackProperties.uri).build();
}
}
......@@ -33,5 +33,5 @@ public interface MediaItemConverter {
* Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.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;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
......@@ -95,7 +94,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
// Should be only accessed on the executor, which is currently single-threaded.
@Nullable private MediaItem currentMediaItem;
@Nullable private List<MediaItem> currentPlaylist;
/**
* Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands.
......@@ -124,19 +122,8 @@ public final class SessionPlayerConnector extends SessionPlayer {
taskHandler = new PlayerHandler(player.getApplicationLooper());
taskHandlerExecutor = taskHandler::postOrRun;
ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener();
PlayerWrapper playerWrapper =
new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
this.player = playerWrapper;
this.player = new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher);
playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler);
@SuppressWarnings("assignment.type.incompatible")
@Initialized
SessionPlayerConnector initializedThis = this;
initializedThis.<Void>runPlayerCallableBlocking(
/* callable= */ () -> {
playerWrapper.reset();
return null;
});
}
@Override
......@@ -258,7 +245,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
ListenableFuture<PlayerResult> result =
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_MEDIA_ITEM, () -> player.setMediaItem(item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
......@@ -281,7 +267,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_SET_PLAYLIST,
/* command= */ () -> player.setPlaylist(playlist, metadata));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
......@@ -294,7 +279,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM,
/* command= */ () -> player.addPlaylistItem(index, item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
......@@ -305,7 +289,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM,
/* command= */ () -> player.removePlaylistItem(index));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
......@@ -318,7 +301,6 @@ public final class SessionPlayerConnector extends SessionPlayer {
playerCommandQueue.addCommand(
PlayerCommandQueue.COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM,
/* command= */ () -> player.replacePlaylistItem(index, item));
result.addListener(this::handlePlaylistChangedOnHandler, taskHandlerExecutor);
return result;
}
......@@ -385,7 +367,7 @@ public final class SessionPlayerConnector extends SessionPlayer {
@Override
@Nullable
public List<MediaItem> getPlaylist() {
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getCachedPlaylist);
return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist);
}
@Override
......@@ -558,25 +540,18 @@ public final class SessionPlayerConnector extends SessionPlayer {
}
private void handlePlaylistChangedOnHandler() {
List<MediaItem> currentPlaylist = player.getCachedPlaylist();
boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist);
this.currentPlaylist = currentPlaylist;
List<MediaItem> currentPlaylist = player.getPlaylist();
MediaMetadata playlistMetadata = player.getPlaylistMetadata();
MediaItem currentMediaItem = player.getCurrentMediaItem();
boolean notifyCurrentMediaItem = !ObjectsCompat.equals(this.currentMediaItem, currentMediaItem);
this.currentMediaItem = currentMediaItem;
if (!notifyCurrentMediaItem && !notifyCurrentPlaylist) {
return;
}
long currentPosition = getCurrentPosition();
notifySessionPlayerCallback(
callback -> {
if (notifyCurrentPlaylist) {
callback.onPlaylistChanged(
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
}
callback.onPlaylistChanged(
SessionPlayerConnector.this, currentPlaylist, playlistMetadata);
if (notifyCurrentMediaItem) {
Assertions.checkNotNull(
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